Metering architecture for usage-based billing
Usage-based pricing fails in a specific way: the invoice is questioned, and nobody can reconstruct how the number was produced. The fix is architectural rather than commercial — meter at the source, store raw events immutably, and derive every aggregate so it can be rebuilt and defended line by line.
Subscription billing is comparatively easy: a plan, a price, a renewal date. Usage-based billing is where SaaS products acquire quiet revenue leakage and loud customer disputes, because the amount owed is computed from events rather than looked up from a contract.
Design for one question
Every decision below follows from a single requirement: when a customer asks why this month's invoice is higher than last month's, you must be able to answer from data rather than from argument. That means the billable events themselves have to survive, not just the totals derived from them.
If the only record of usage is the number on the invoice, the invoice is not evidence — it is an assertion.
Meter at the source, not afterwards
The most common mistake is inferring usage later from something that was recorded for another purpose — counting rows in an audit log, parsing request logs, or summing a mutable counter. Each of those drifts from the thing you are actually charging for.
Emit the event at the point the billable thing happens, from the code path that performs it, carrying everything needed to price it later.
Idempotency is the whole game
Usage events arrive over networks that retry. Without a uniqueness constraint keyed on something the caller controls, a retry becomes double billing, which is the single fastest way to lose a customer's trust in your invoicing.
- The caller generates the idempotency key, deterministically from the operation — not randomly per attempt
- Uniqueness is enforced by a database constraint, not by an application-level existence check that races
- A duplicate insert is a success, not an error — return the original event rather than failing the caller
- The key is scoped per tenant, so two tenants cannot collide
Aggregate derivably, never destructively
Querying millions of raw events at invoice time does not scale, so you will aggregate. The rule is that every aggregate must be reproducible from the raw events, and no aggregation step may be the only place a number exists.
Because the rollup is derived and the recompute is idempotent, a bug in aggregation is a recoverable incident rather than permanent data loss. That property is worth more than any amount of query optimisation.
Late events and closed periods
Events will arrive after the period they belong to has been invoiced. Decide the policy explicitly, write it down, and make the system implement exactly that policy rather than whatever the code happens to do.
- Define a grace window — say 48 hours — during which late events still land in their original period.
- After the period closes, late events are attributed to the current period instead, with metadata recording the period they actually occurred in.
- Never silently mutate a period that has already been invoiced; an invoice you have sent is a statement of fact.
- Alert when late events exceed a threshold — it usually means an integration is failing, not that customers changed behaviour.
Reconcile against the billing provider
Whatever you push to Stripe, Razorpay or an invoicing system becomes a second source of truth, and the two will diverge. Reconciliation is a scheduled job, not a one-off migration script.
Show the customer the working
The strongest anti-dispute measure is making usage visible before the invoice arrives. A usage page showing current-period consumption by metric and day, drawn from the same rollups that produce the invoice, converts billing surprises into something the customer already knew about.
It also serves as continuous verification: customers notice anomalies in their own usage far faster than your monitoring will, and they will tell you.
Questions
Should usage events go through a queue or straight into the database?
Straight into the database is usually correct to start with — an append-only insert with a uniqueness constraint is fast, and it removes an entire class of "the queue lost it" failures. Introduce a queue when ingestion volume genuinely threatens the transactional database, and keep the same idempotency key so replay stays safe.
How long should raw usage events be retained?
At minimum, long enough to cover your longest plausible billing dispute plus any statutory record-keeping obligation — in practice this tends to mean years, not months. Raw events are small and compress well; deleting them to save storage removes the only evidence that supports your invoices.
Can the billing provider be the source of truth instead?
Not safely. Providers aggregate what you send them and generally do not retain per-event detail with your own metadata attached. Keep the authoritative raw record yourself and treat the provider as a downstream system you reconcile against — which also means you can change provider without losing billing history.
How do you handle a customer who disputes a charge?
Query the raw events for that tenant, metric and period, and show the count and total alongside the aggregate that produced the invoice. If the two agree, the invoice is defensible. If they disagree, you have found a real bug and the customer is right — which is exactly why the raw events exist.