Implement Usage-Based Billing
10 Aug 2026Usage-based billing sounds simple until you build it: meter something, multiply by a rate, charge at the end of the month. The hard parts are the ones nobody mentions — what counts as a unit, what happens when metering lags, and what you do when a customer disputes the number.
Pick the unit before you pick the tool
Every usage-billing disaster starts with an ambiguous unit. “API calls” is not a unit — is a failed request a call? A retried request? A request that hits your cache and never touches the database? Write the definition down before you write a line of metering code:
A billable request = any request that returns 2xx or 4xx and
reaches the application layer. 5xx responses are not billed.
Cached responses (X-Cache: HIT) are not billed.
Retries share the idempotency key of the original request and
are billed once.
Put this definition on a public pricing/docs page. When a customer disputes their bill, you point at the definition instead of arguing from memory.
Meter at the edge, aggregate in a queue
Don’t write directly to your billing table on every request — it doesn’t scale and it couples your hot path to your billing system’s uptime. Emit an event, aggregate asynchronously:
// on every billable request
await redis.incr(`usage:${customerId}:${currentHour()}`)
Then a periodic job rolls hourly counters into a daily total, and daily into the invoice period:
// cron, once per hour
const keys = await redis.keys('usage:*:' + previousHour())
for (const key of keys) {
const [, customerId] = key.split(':')
const count = await redis.get(key)
await db.usageDaily.upsert({ customerId, date: today(), count })
await redis.del(key)
}
This gives you a buffer: if the aggregation job fails, you haven’t lost the raw counts, and you can re-run it. Writing straight to Postgres on every request gives you neither the buffer nor the throughput.
Stripe’s metered billing, briefly
If you’re on Stripe, use meter_events on a metered price instead of building your own aggregation from scratch:
await stripe.billing.meterEvents.create({
event_name: 'api_requests',
payload: {
stripe_customer_id: customerId,
value: count,
},
})
Stripe aggregates and bills at period end. You still need your own event log — Stripe’s meter is not queryable in the granular way you’ll want when a customer asks “what did I get charged for on the 14th.”
Handle the dispute before it happens
Every usage-billing product eventually gets “this number is wrong.” Build the tool to answer it before the first ticket, not after:
- Keep raw, timestamped usage events for at least one full billing cycle past the invoice.
- Give customers (or yourself, minimum) a way to query usage by day, not just see a monthly total.
- Decide your correction policy up front — do you refund a proven overcharge, or credit next month? Pick one and put it in your terms.
The plan-change trap
If usage resets or prorates on a plan change mid-cycle, decide the rule before someone hits it: does usage-to-date carry over, or restart? Most SaaS billing bugs live in this exact seam — the customer upgrades on day 15, and now your system has two partial periods with two different rates to reconcile. Write the rule down, write a test for it, and don’t leave it to whatever the code happens to do.
Don’t build metering you don’t need yet
If you have fewer than a dozen customers on usage-based pricing, a nightly script that sums a Postgres table and writes a CSV is enough. Real-time counters, Redis, and Stripe meter events are worth the complexity once metering volume or dispute frequency actually demands it — not before.