IAnswerable

Implement Usage-Based Billing

10 Aug 2026

Usage-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:

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.