Exclusive Discount Deal
Upto 50% OFF
Offer ends in:
10 DAYS
|
21 HOURS
|
19 MINS
|
05 SECS
Home / Blog / Implementing Usage Based Metered Billing With: Architectural Bluepri
SaaS & Platforms • Oct 21, 2026

Implementing Usage Based Metered Billing With: Architectural Bluepri

Practical engineering guide and architectural blueprint for Implementing Usage Based Metered Billing With: Architectural Bluepri.

UPTO 50% OFF
Trending:
BrickTry

AI is analyzing your requirement...

Generating custom modules, implementation options, and dynamic clarification questions.

Add Custom Requirement or Module

Add your own specific features, integrations, or components. AI will incorporate them to dynamically generate the next relevant options.

1. Progressive Clarifications

Click to expand & answer

2. Scope Modules & Features (/ Selected)

Click row to expand details · Customize options
✓
✕

The transition from static seat-based SaaS subscriptions to consumption-driven pricing (such as compute-seconds, API invocation volume, storage gigabyte-hours, or LLM token counts) introduces a complex distributed systems problem.

At scale, naive implementations—such as sending HTTP POST calls synchronously to payment gateway APIs (e.g., Stripe, Lago, Metronome) on every user action—invariably break down. Payment processor APIs enforce strict rate limits (typically 100 requests/second), add hundreds of milliseconds of latency to critical application paths, and expose billing pipelines to network edge failures.

To build a reliable, high-throughput metered billing system, systems architects must decouple usage ingestion from billing reconciliation. This blueprint provides a production-ready reference architecture for an event-driven metered billing engine capable of processing millions of daily events with sub-millisecond ingestion overhead, zero event loss, and strict idempotency guarantees.


Architectural Principles of Metered Billing Systems

A resilient usage-based billing infrastructure requires four architectural layers:

[ Edge API / App Microservices ]
               │ (Async / Low-latency Event Push)
               ▼
┌───────────────────────────────────────────────────────────┐
│ 1. INGESTION & DEDUPLICATION LAYER                       │
│    - Fast write-path buffer (Redis Streams / Apache Kafka) │
│    - Deduplication filter via Unique Event ID             │
└──────────────────────────────┬────────────────────────────┘
                               │
                               ▼
┌───────────────────────────────────────────────────────────┐
│ 2. AGGREGATION ENGINE                                     │
│    - In-Memory Bucket Aggregation (Redis Hashes / Flink)  │
│    - Tumbling / Sliding Windows (e.g., 5-minute buckets)   │
└──────────────────────────────┬────────────────────────────┘
                               │
                               ▼
┌───────────────────────────────────────────────────────────┐
│ 3. IMMUTABLE AUDIT LEDGER                                 │
│    - Relational Storage (PostgreSQL DB with append-only)  │
│    - Reconciles raw metrics for compliance & auditing    │
└──────────────────────────────┬────────────────────────────┘
                               │
                               ▼
┌───────────────────────────────────────────────────────────┐
│ 4. PAYMENT GATEWAY DISPATCHER                             │
│    - Scheduled Batch Sync to Stripe / Billing API         │
│    - Distributed Locks & Idempotent Usage Record Creation│
└───────────────────────────────────────────────────────────┘
  1. Ingestion Layer: Accepts raw events via low-latency transports (Redis Streams, Kafka, or AWS Kinesis). It guarantees sub-5ms response times to application callers and performs initial payload validation.
  2. Aggregation Engine: Computes running usage totals over discrete time buckets using atomic, in-memory datastructures. This isolates upstream databases from high-frequency write operations.
  3. Immutable Audit Ledger: Persists raw event streams and periodic snapshots into append-only relational database tables. This provides compliance auditing, chargeback dispute resolution, and historical reporting.
  4. Gateway Synchronizer: An asynchronous background engine that reads aggregated totals, formats them to match billing provider schemas, and submits them using idempotent batch requests.

Architectural Comparison of Metered Billing Approaches

Selecting the right design pattern depends on event volume, tolerance for latency, and compliance demands.

Feature / Dimension Synchronous Gateway Direct Push Redis In-Memory Aggregation + Worker Distributed Stream Processing (Kafka + Flink + SQL)
Ingestion Throughput Low (< 100 req/sec) High (10,000–50,000 ops/sec) Ultra-High (> 100,000 ops/sec)
P99 Ingestion Latency High (200ms - 1500ms) Extremely Low (< 2ms) Low (< 10ms)
Gateway Rate Limit Risk Critical (Immediate failure) Zero (Controlled batching) Zero (Controlled batching)
Data Loss Resistance Vulnerable to network drops High (with Redis AOF / Replication) Maximum (Distributed log persistence)
System Complexity Minimal (Monolithic) Moderate (Standard SaaS Production) High (Requires dedicated infrastructure)
Auditability & Replay Non-existent High (via DB Audit Ledger) Total (Full stream replay capability)

For 95% of growth and enterprise SaaS platforms processing between 100,000 and 50,000,000 events daily, the Redis In-Memory Aggregation + Resilient Batch Worker pattern provides the ideal balance of reliability, low operational overhead, and cost efficiency.


Ingestion & Aggregation Engine Implementation

Below is a production-ready Node.js/TypeScript implementation using ioredis. This service exposes a atomic event ingestion function that writes to a sliding time-bucket hash while enforcing event deduplication using atomic Lua scripting.

Ingestion Script: usageAggregator.ts

import Redis from 'ioredis';

export interface UsageEvent {
  eventId: string;
  tenantId: string;
  metric: string;
  value: number;
  timestamp: number; // Unix Epoch in seconds
}

export class UsageAggregator {
  private redis: Redis;

  constructor(redisUrl: string) {
    this.redis = new Redis(redisUrl, {
      enableAutoPipelining: true,
      maxRetriesPerRequest: 3,
    });
  }

  /**
   * Records a usage event atomically using Lua scripting.
   * Guarantees idempotency via eventId sets and aggregates into 5-minute windows.
   */
  public async recordUsage(event: UsageEvent): Promise<boolean> {
    const windowSizeSeconds = 300; // 5-minute aggregation buckets
    const bucketTimestamp = Math.floor(event.timestamp / windowSizeSeconds) * windowSizeSeconds;

    const dedupKey = `dedup:${event.tenantId}:${event.eventId}`;
    const bucketKey = `usage:${event.tenantId}:${event.metric}:${bucketTimestamp}`;

    // Atomic Lua Script: Check deduplication key; if absent, increment hash metric and set TTL.
    const luaScript = `
      if redis.call("EXISTS", KEYS[1]) == 1 then
        return 0;
      end
      redis.call("SET", KEYS[1], "1", "EX", 86400) -- Deduplication TTL: 24 Hours
      redis.call("HINCRBYFLOAT", KEYS[2], "total_value", ARGV[1])
      redis.call("HINCRBY", KEYS[2], "event_count", 1)
      redis.call("EXPIRE", KEYS[2], 604800) -- Bucket TTL: 7 Days
      return 1;
    `;

    const result = await this.redis.eval(
      luaScript,
      2,
      dedupKey,
      bucketKey,
      event.value.toString()
    );

    return result === 1;
  }

  public async getAggregatedUsage(tenantId: string, metric: string, bucketTimestamp: number): Promise<{ totalValue: number; eventCount: number }> {
    const bucketKey = `usage:${tenantId}:${metric}:${bucketTimestamp}`;
    const data = await this.redis.hmget(bucketKey, 'total_value', 'event_count');

    return {
      totalValue: data[0] ? parseFloat(data[0]) : 0,
      eventCount: data[1] ? parseInt(data[1], 10) : 0,
    };
  }
}

Gateway Sync Engine: Asynchronous Billing Synchronization

Once usage is buffered and aggregated in memory, a background worker flushes closed aggregation windows, updates a PostgreSQL immutable audit ledger, and submits the usage records to Stripe using idempotency keys.

Synchronization Worker: stripeSyncWorker.ts

import Stripe from 'stripe';
import { Pool } from 'pg';
import Redis from 'ioredis';

export class StripeSyncWorker {
  private stripe: Stripe;
  private db: Pool;
  private redis: Redis;

  constructor(stripeApiKey: string, dbPool: Pool, redisUrl: string) {
    this.stripe = new Stripe(stripeApiKey, { apiVersion: '2024-12-18.acacia' });
    this.db = dbPool;
    this.redis = new Redis(redisUrl);
  }

  /**
   * Flushes aggregated metric buckets to Stripe and commits to local audit DB.
   */
  public async syncBucketToStripe(
    tenantId: string,
    stripeSubscriptionItemId: string,
    metric: string,
    bucketTimestamp: number
  ): Promise<void> {
    const bucketKey = `usage:${tenantId}:${metric}:${bucketTimestamp}`;
    const lockKey = `lock:sync:${tenantId}:${metric}:${bucketTimestamp}`;

    // Acquire distributed lock to prevent concurrent sync workers from processing the same bucket
    const acquiredLock = await this.redis.set(lockKey, 'locked', 'NX', 'EX', 120);
    if (!acquiredLock) {
      console.log(`[Sync Worker] Bucket ${bucketKey} is currently locked by another worker.`);
      return;
    }

    try {
      const usageData = await this.redis.hmget(bucketKey, 'total_value', 'event_count');
      const totalValue = usageData[0] ? parseFloat(usageData[0]) : 0;
      const eventCount = usageData[1] ? parseInt(usageData[1], 10) : 0;

      if (totalValue === 0 || eventCount === 0) {
        return; // Nothing to sync
      }

      // Generate deterministically idempotent key for Stripe
      const idempotencyKey = `stripe_usage_${tenantId}_${metric}_${bucketTimestamp}`;

      // 1. Dispatch usage record to Stripe API
      const usageRecord = await this.stripe.subscriptionItems.createUsageRecord(
        stripeSubscriptionItemId,
        {
          quantity: Math.ceil(totalValue),
          timestamp: bucketTimestamp,
          action: 'set', // Options: 'increment' or 'set'
        },
        {
          idempotencyKey: idempotencyKey,
        }
      );

      // 2. Persist into SQL Immutable Audit Ledger in a transaction
      const client = await this.db.connect();
      try {
        await client.query('BEGIN');

        const insertLedgerQuery = `
          INSERT INTO usage_ledger (tenant_id, metric, bucket_timestamp, quantity, event_count, stripe_record_id)
          VALUES ($1, $2, to_timestamp($3), $4, $5, $6)
          ON CONFLICT (tenant_id, metric, bucket_timestamp)
          DO UPDATE SET quantity = EXCLUDED.quantity, event_count = EXCLUDED.event_count;
        `;
        await client.query(insertLedgerQuery, [
          tenantId,
          metric,
          bucketTimestamp,
          totalValue,
          eventCount,
          usageRecord.id,
        ]);

        // Clear or flag processed key in Redis
        await this.redis.hset(bucketKey, 'synced_to_stripe', 'true');
        await client.query('COMMIT');
      } catch (dbError) {
        await client.query('ROLLBACK');
        throw dbError;
      } finally {
        client.release();
      }

    } catch (error) {
      console.error(`[Sync Worker Error] Failed to sync usage bucket ${bucketKey}:`, error);
      throw error;
    } finally {
      await this.redis.del(lockKey);
    }
  }
}

Edge Cases and Failure Modes

1. Late-Arriving Events Across Billing Boundaries

Events delayed by client connectivity drops or offline mobile devices may arrive after a billing period closes in Stripe.

  • Mitigation: Maintain a strict lookback window (e.g., 24 hours). If an event timestamp falls into a previously billed Stripe subscription cycle, do not attempt to write to the old subscription item. Instead, redirect the metric to the current billing period with a late_adjustment audit tag, or generate an out-of-band invoice item (stripe.invoiceItems.create).

2. Double Billing and Idempotency Breakdown

If a synchronization worker crashes immediately after calling Stripe but before executing the database commit, re-running the job risks duplicate charges.

  • Mitigation: Rely on Stripe's native idempotencyKey parameter. By calculating the key deterministically from structural domain metadata (stripe_usage_${tenantId}_${metric}_${bucketTimestamp}), subsequent retries automatically return the original response without duplicating records.

3. High-Cardinality Metrics Scaling Limits

If tracking metrics across millions of individual resources per tenant (e.g., per-file or per-device tracking), Redis hash sizes can bloat memory capacity.

  • Mitigation: Aggregate at high-cardinality targets locally using dynamic dynamic bloom filters or hyperloglog counters, flushing only rolled-up metric dimensions to primary Redis data stores.

How BrickTry Helps You Build and Scale This

Architecting, testing, and hardening a distributed usage-based billing platform requires significant domain expertise across database transactional guarantees, stream aggregation, and third-party payment integration limits. BrickTry accelerates the deployment of high-throughput SaaS infrastructure through a cohesive suite of platform capabilities:

  • Interactive In-Browser Lab Sandbox (/lab): Instantly spin up isolated Node.js, Redis, and PostgreSQL microservices within an interactive in-browser container runtime. Test multi-tenant ingestion throughput, model latency under edge conditions, and preview billing event flows in real-time with zero local environment configuration.
  • AI-Human Dev Pairing: Accelerate construction using BrickTry's autonomous scaffolding agents to auto-generate fully typed TypeScript client schemas, SQL ledger migrations, and API routes. Dedicated senior platform engineers review system architecture, audit Lua concurrency scripts, and verify edge-case recovery logic prior to production release.
  • Interactive Scoping Engine: Translate complex meter pricing models (tiered, volume-based, dynamic seat allocations, overage caps) into standardized database entities, event schemas, and production readiness checklists in minutes.
  • 100% Intellectual Property Ownership: BrickTry delivers production codebases direct to your private GitHub repository with standardized Docker, Kubernetes, and Terraform assets. You maintain total IP ownership with absolute operational independence and zero vendor lock-in.

Frequently Asked Questions

1. Should usage records in Stripe be updated using the 'increment' or 'set' action?

Using action: 'set' alongside deterministic aggregation buckets (e.g., 5-minute or 1-hour fixed windows) is recommended over action: 'increment'. If a retry occurs with action: 'increment' and an idempotency key expires, duplicate usage may be appended. action: 'set' combined with windowed aggregation is inherently state-declarative and resilient against network duplicate dispatches.

2. How should database schemas be structured for high-volume billing logs?

Use an append-only relational table partitioned by time (e.g., monthly PostgreSQL hyper-tables or native declarative partitioning). Index on (tenant_id, metric, bucket_timestamp). Avoid issuing raw UPDATE statements on high-frequency write paths; insert immutable event records and aggregate asynchronously via database background processes or dedicated worker queues.

3. What is the optimal strategy for real-time customer usage limit checks?

Do not query Stripe or perform expensive SUM() queries across SQL ledger tables during active API request execution. Instead, store current usage balances in Redis hashes. On incoming API requests, perform an atomic check using HGET or INCRBY against pre-configured quota limits. If exceeded, return an HTTP 429 Too Many Requests status instantly without reaching underlying backend microservices or payment gateways.

Build, Test, and Scale This on BrickTry

BrickTry pairs you with autonomous AI scaffolding supervised by dedicated senior full-stack software engineers in an interactive in-browser development sandbox (/lab). Test, build, and deploy production-grade software with 100% source code ownership and zero vendor lock-in.

Launch Interactive Requirement Builder →

❤️

Support BrickTry Platform & Engineering Development

Help us build, maintain, and advance our AI engineering platform. Every donation fuels open-source tooling, infrastructure, and continuous improvements.

$
Donor Details
Promote Your Brand / Link Wall

UPI / Credit & Debit Cards / Netbanking
Razorpay
Secure 256-bit encrypted checkout
View Leaderboard & Wall

Hey!

Welcome, Let's chat —
start a new conversation
below.

Recent conversations
See all

We’re online to assist you with your project...

Abhishek A Agrawal • Just now

Start a conversation

Quick contact setup

Please share your details below so our team can reach you.

Worldwide supported

🔒 Your info is only used to connect with our support team.

Abhishek A Agrawal

Online & Ready to Assist

BrickTry

Select a Date & Time Enter Details You are Scheduled

Technical Consultation

Video link provided upon confirmation
SUN
MON
TUE
WED
THU
FRI
SAT
Time zone:

,

•

Google Meet
Default HD Video
Calendar invitations with direct video links will be sent here.

You are scheduled

A calendar invitation has been sent to your email address.