Building a metered, consumption-based pricing tier requires a deterministic approach to usage tracking. When thousands of concurrent API requests or compute operations flow through your platform, counting events directly inside a relational database will cause database connection exhaustion, race conditions, and billing drift.
To maintain financial accuracy without sacrificing system performance, you must decouple event ingestion from Stripe's API. This requires an event-driven architecture using Redis as an ingestion buffer, a worker queue for asynchronous aggregation, and Stripe Connect or Stripe Metered Billing to execute monthly invoicing.
Architectural Topology
High-concurrency usage tracking relies on a three-tier design:
- Ingestion Layer: API gateways or middleware that push incremental counters to an in-memory datastore with sub-millisecond latency.
- Aggregation Layer: Background workers that flush, window, and roll up raw metric events into billable aggregates.
- Synchronization Layer: Idempotent cron or event-driven sync jobs that push finalized aggregates to Stripe via the Usage Records API.
[ Client Request ]
│
▼
[ API Gateway / Middleware ]
│ (Atomic INCRBY)
▼
┌─────────────────────────┐
│ Redis Cluster │ ◄── [ Periodic Flush Worker (Cron / BullMQ) ]
└─────────────────────────┘
│
▼
┌───────────────────────────────┐
│ PostgreSQL Billing Ledger │
└───────────────────────────────┘
│
▼
┌───────────────────────────────┐
│ Stripe Metered API │
└───────────────────────────────┘
1. High-Performance Ingestion with Redis
To capture usage without blocking client threads, avoid writing individual usage rows to PostgreSQL or MySQL on every API call. Instead, utilize Redis HINCRBY or sorted sets to increment counters in memory.
The following TypeScript snippet demonstrates a fast middleware function that records metered feature consumption (e.g., LLM tokens or API call counts) atomically in Redis:
import { Redis } from 'ioredis';
import { Request, Response, NextFunction } from 'express';
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
interface MeteredRequest extends Request {
tenantId?: string;
meterKey?: string;
usageQuantity?: number;
}
export async function meterUsageMiddleware(req: MeteredRequest, res: Response, next: NextFunction) {
const tenantId = req.tenantId;
const meterKey = req.meterKey || 'api_calls';
const quantity = req.usageQuantity || 1;
if (!tenantId) {
return res.status(400).json({ error: 'Missing tenant context for metering.' });
}
const timestampWindow = Math.floor(Date.now() / 3600000); // Hourly window
const redisKey = `usage:${tenantId}:${meterKey}:${timestampWindow}`;
try {
// Atomic increment in Redis; O(1) time complexity
await redis.hincrby(redisKey, 'count', quantity);
// Set a TTL of 7 days to prevent memory leaks from stale tenants
await redis.expire(redisKey, 604800);
next();
} catch (error) {
// Fail open or closed depending on business requirements;
// here we log and proceed to prevent billing downtime from breaking core app UX.
console.error(`[Metering Error] Failed to record usage for tenant ${tenantId}:`, error);
next();
}
}
2. Idempotent Aggregation & Stripe Sync Worker
At the end of each billing cycle or hourly window, a background worker must flush Redis metrics into a durable ledger and transmit them to Stripe. Idempotency is critical here; network drops or worker retries must never double-bill a customer.
Below is a robust Node.js worker implementation using Stripe's Node SDK and cryptographic idempotency keys derived from the tenant, meter, and time window:
import Stripe from 'stripe';
import { Redis } from 'ioredis';
import crypto from 'crypto';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2023-10-16' });
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
interface FlushJob {
tenantId: string;
stripeSubscriptionItemId: string;
meterKey: string;
windowTimestamp: number;
}
export async function processUsageFlush(job: FlushJob): Promise<void> {
const { tenantId, stripeSubscriptionItemId, meterKey, windowTimestamp } = job;
const redisKey = `usage:${tenantId}:${meterKey}:${windowTimestamp}`;
// Fetch current accumulated count
const rawCount = await redis.hget(redisKey, 'count');
const quantity = rawCount ? parseInt(rawCount, 10) : 0;
if (quantity === 0) return;
// Generate a deterministic idempotency key to prevent duplicate Stripe billing events
const idempotencyKey = crypto
.createHash('sha256')
.update(`${tenantId}-${stripeSubscriptionItemId}-${windowTimestamp}`)
.digest('hex');
try {
// Send usage record to Stripe Billing API
await stripe.subscriptionItems.createUsageRecord(
stripeSubscriptionItemId,
{
quantity,
timestamp: windowTimestamp,
action: 'increment',
},
{
idempotencyKey,
}
);
// Mark key as synchronized in Redis or delete/archive it
await redis.hset(redisKey, 'synced', '1');
console.log(`[Stripe Sync] Successfully billed ${quantity} units for tenant ${tenantId}`);
} catch (error: any) {
if (error.code === 'idempotency_key_in_use') {
console.warn(`[Stripe Sync] Duplicate sync attempt avoided via idempotency key: ${idempotencyKey}`);
return;
}
console.error(`[Stripe Sync Failed] Tenant ${tenantId}:`, error.message);
throw error; // Trigger worker retry queue
}
}
3. Data Architecture & Index Strategies
To reconcile discrepancies between Redis, your internal relational ledger, and Stripe's invoices, maintain a persistent audit table in PostgreSQL.
| Database Table | Column Name | Data Type | Index Strategy | Purpose |
|---|---|---|---|---|
usage_ledgers |
id |
UUID | Primary Key | Unique row identifier |
usage_ledgers |
tenant_id |
VARCHAR(64) | B-Tree (Composite) | Tenant query partitioning |
usage_ledgers |
meter_key |
VARCHAR(64) | B-Tree (Composite) | Feature classification |
usage_ledgers |
window_start |
TIMESTAMP | B-Tree | Temporal range queries |
usage_ledgers |
stripe_synced |
BOOLEAN | Partial Index (WHERE synced = false) |
Fast lookup for failed sync queues |
How BrickTry Accelerates & Powers This
Building enterprise-grade metered billing pipelines from scratch requires orchestrating Redis clusters, asynchronous queue workers, Stripe webhooks, and secure database migrations. BrickTry eliminates this infrastructure boilerplate so your engineering team can focus on core product value.
- BrickTry Lab Sandbox (
/lab): Test your Redis ingestion middleware and Stripe webhook handlers instantly in an isolated zero-setup in-browser Node/Vite virtual container runtime, complete with live logs and terminal access. - AI-Human Dev Pairing: Let autonomous AI scaffolding generate initial database schemas, Stripe Connect onboarding flows, and BullMQ worker configurations, while our senior full-stack engineering pods review your architecture for edge cases, race conditions, and security vulnerabilities.
- Interactive Scoping Engine: Break down complex multi-tenant billing requirements into modular architectural milestones, automated schema generators, and production-ready deployment checklists.
- Unified Importer: Seamlessly import existing GitHub repositories or legacy CodeCanyon billing scripts, automatically refactoring monolithic codebases into modern clean architecture.
- 100% Source Code Ownership: Maintain complete sovereignty over your intellectual property with full ownership of GitHub repositories, Docker configurations, and PostgreSQL schemas, ensuring zero vendor lock-in.
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. Test, build, and deploy production-grade software with 100% source code ownership and zero vendor lock-in.