Metering is counting what each tenant uses; usage-based billing turns those counts into money on an invoice. Between the two sits a pipeline with awkward requirements: it must never slow down the requests it measures, it must count every billable unit exactly once even though messages get retried, and it must produce invoices that a customer’s finance team can check line by line.
Picture it first: the electricity meter in a housing society
A housing society in Pune has 240 flats, one transformer and a meter in every flat.
The meter keeps a running total all month and has no idea what a unit costs. Once a month the meter reader notes each reading, and the bill is this month’s reading minus last month’s. If the reader writes the same reading down twice, nobody pays twice: a bill is a difference between readings, not a sum of visits. The tariff turns units into rupees in slabs: the first 100 units at one rate, the next 200 at another. The printed bill can’t be quietly changed once issued; a mistake is fixed with a credit on the next one. And the MCB in each flat trips within milliseconds when a flat draws too much current. It never waits for the bill and never produces one.
Map it back. The meter is your usage events, the reading is aggregation per period, the tariff is the price plan, the bill is the invoice and its correction a credit note. The MCB is enforcement: quotas and rate limits that act in milliseconds on approximate numbers. A repeated reading that changes nothing is idempotent aggregation.
Four jobs, four clocks
Teams say “billing” for four jobs that need different freshness and different correctness.
| Job | Question | Freshness | Correctness | Typical failure |
|---|---|---|---|---|
| Metering | What happened, for whom? | Seconds to minutes | Every billable unit counted once | Lost or doubled events |
| Rating | What is it worth? | At period close; on demand for previews | Exact, reproducible from the price version in force | Wrong price version |
| Invoicing | What do we ask the customer to pay? | Once a period, after a grace period | A legal document, final once issued | Late usage with nowhere to go |
| Enforcement | May this request run? | Milliseconds | Approximate is fine | A quota trips late or early |
Why meter at all? Usually for several reasons at once:
- Usage-based or hybrid pricing: per API call, GB or token, or a subscription with an included allowance and overage.
- Entitlements and quotas: the free plan includes 10,000 calls a month, Pro 500,000.
- Cost per tenant: what each tenant costs to serve, which can differ wildly from what it pays.
- Fair use: spotting the tenant whose usage hurts others (noisy neighbours and fairness).
In the interview
Say the split out loud: “Enforcement must be fast and can be slightly wrong. Invoices must be exact and can be slightly late. So they are two systems fed by the same usage, and neither reads the other’s numbers on the hot path.”
What to meter
A billable metric is the unit on the invoice, so it has to make sense to the person paying. Good ones share four traits: the customer understands it, can predict it, can control it, and it grows with the value they get. API calls, seats, messages sent, GB-months stored, build minutes and, for an LLM product, input and output tokens all pass. CPU-seconds inside your own microservices fail: the customer can’t predict them, and every optimisation you ship changes their bill.
Internal cost signals are metered too, for a different consumer: CPU time, database time and bytes moved per tenant feed cost per tenant and noisy-neighbour detection. They can be sampled and approximate; billable metrics can’t.
Each meter also needs an aggregation. Stripe offers three: sum (tokens, calls), count (events) and last (a value such as seats in use). A gauge such as storage needs a time average. A tenant storing 500 GB for 10 days and 800 GB for the other 20 days of a 30-day month used (500 × 10 + 800 × 20) ÷ 30 = 700 GB-months. Sample gauges on a fixed schedule and bill the average, not the peak or the last reading.
The usage event
Everything starts with one record, emitted where the billable work happened:
{
"event_id": "01JB7X3K9Q2M4T6V8W0Y1Z3A5C",
"tenant_id": "t_7f3a",
"meter": "llm_output_tokens",
"quantity": 1840,
"occurred_at": "2026-10-31T23:59:58.412Z",
"source": "inference-gw-12",
"properties": { "model": "large", "region": "ap-south-1" }
}
event_idis unique, created once at the source and reused on every retry. Stripe calls itidentifierand enforces uniqueness over a rolling window of at least 24 hours; OpenMeter, following CloudEvents, treats events with the samesourceandidas one.occurred_atis event time, when the work happened, not when the event arrived. It decides which billing period the usage belongs to.quantityis a number. A correction is a new event with a negative quantity, never an edit.propertiescarry only the dimensions pricing needs. Each one multiplies the rows you aggregate, and Stripe caps a meter at 10,000 distinct dimension combinations an hour and 100 per customer.
Mint the ID before the first send
If the ID is generated inside the send function, every retry gets a fresh one and becomes a second charge. Create it when the work completes, store it with the event in the spool, and resend it unchanged.
Emit asynchronously. The request path appends the event to a buffer and returns; a shipper sends batches in the background. Billing must never slow down or fail a customer’s request, the same rule that keeps the control plane off the request path. If the billable work is a database change, such as a document created or a message queued, write the usage event in the same transaction with a transactional outbox, so the work and its charge can’t disagree. And emit once the outcome is known: a request that failed with a 5xx usually shouldn’t be billed.
The pipeline, from event to invoice
- Emit into a local spool: memory first, then disk, so a node can buffer through an outage of the event bus.
- Ship batches to a durable event log such as Kafka, keyed by tenant so each tenant’s events land in one partition, in order.
- Dedupe and sum. A stream job drops IDs it has already seen and adds the rest to a per-minute total for each tenant and meter.
- Store the totals in an append-only usage ledger, which also feeds the customer’s dashboard. Every raw event is copied to cheap object storage.
- Rate at period close with the price version in force when the usage happened.
- Invoice after a grace period for late events.
Exactly once is a property of the total
No network delivers exactly once. A producer that crashes after sending but before the acknowledgement must resend, and a consumer that crashes after updating a total but before committing its offset sees the event again. So deliver at least once and make the consumer idempotent: record the event ID and add its quantity in one transaction, and skip IDs already recorded. Duplicates then change nothing: the effectively-once pattern. Stripe’s pre-aggregated mode shows the other idempotent shape: send the running total for an hour or a day, and a newer event for the same window replaces the older one, like a meter reading.
The marketplaces enforce the same discipline at their front door. Azure accepts one usage event per resource, dimension and hour, within 24 hours, and answers a duplicate with 409 Conflict. AWS’s BatchMeterUsage treats identical requests as idempotent and rejects records 24 hours or more after the usage. Both push you to total by the hour and send once.
Pre-aggregate to cut volume
A product serving 2 billion API calls a day emits 2,000,000,000 ÷ 86,400 ≈ 23,000 events a second on average: 23 times Stripe’s standard limit of 1,000 meter-event calls a second, and more than double the 10,000 a second its high-throughput streams take. Roll up at the source instead. If each of 50 gateway nodes handles 23,000 ÷ 50 = 460 calls a second and sees about 1,000 distinct tenants a minute, it sends 1,000 per-minute rollups instead of 460 × 60 = 27,600 events: about 28 times fewer, or roughly 830 rollups a second across the fleet.
Rollups shrink the dedupe state too. Remembering 16-byte IDs for 7 days of raw events takes 2 billion × 7 × 16 B ≈ 224 GB; for rollups it is 72 million a day × 7 × 16 B ≈ 8 GB. You pay in detail: a rollup can’t say which request cost what, so keep raw request logs for disputes, and give each rollup a deterministic ID (node, tenant, meter, minute) so resending it is harmless.
Late events and closing a period
Events arrive late: a spool drains after an outage, a mobile SDK syncs the next morning, a batch job reports yesterday. Stream processors handle this with event time and watermarks. In Flink’s terms, a watermark for time t declares that no more events stamped at or before t should arrive, and anything that turns up after the watermark has passed is a late element.
Billing adds a hard edge: the period close. October can’t stay open forever, because an invoice is a legal document that has to go out. So you choose a grace period after the period ends: October usage that arrives within it counts in October, and the invoice is finalised when it ends. Real systems choose very different windows:
- Stripe keeps invoices open for 1 hour by default, configurable up to 72 hours. Usage that arrives later still shows in its meter summaries but isn’t added to the finalised invoice or to the next one, so handling it is your job.
- AWS Marketplace accepts the previous month’s usage until 06:00 UTC on the 1st.
- Azure Marketplace accepts nothing older than 24 hours.
For usage that misses the close, never edit a sent invoice. Bill it on the next invoice as a clearly labelled line (“October usage received late: 12,400 calls”), or issue a credit or debit note against the original. Track the late fraction: if it is more than a sliver, the grace period is too short or a producer is broken.
Time zones. “October” starts at different instants for different tenants. If you bill calendar months in each tenant’s zone, aggregation windows must line up with every zone you serve. India is UTC+5:30, so IST midnight falls in the middle of a UTC hour: hourly UTC buckets can’t be split at an IST month boundary, while 15-minute buckets can, and also fit Nepal’s UTC+5:45. The simpler choice is to bill everyone in UTC, as the marketplaces do, and print it on the invoice.
Clock skew puts usage in the wrong period, so check occurred_at against arrival time. Stripe rejects timestamps more than 5 minutes in the future or older than 35 days.
Pricing models, worked
Rating is arithmetic over the aggregated quantity, but the shape of the arithmetic changes what customers pay and how they behave. Take one meter, API calls a month, with three tiers: the first 100,000 calls at ₹0.50, the next 900,000 at ₹0.30, and every call beyond 1,000,000 at ₹0.20.
Graduated pricing charges each tier’s calls at that tier’s price. At 1.2 million calls: 100,000 × ₹0.50 + 900,000 × ₹0.30 + 200,000 × ₹0.20 = ₹50,000 + ₹2,70,000 + ₹40,000 = ₹3,60,000.
Volume pricing charges every call at the price of the tier the total lands in: 1,200,000 × ₹0.20 = ₹2,40,000. Stripe’s docs point out the side effect: the total can fall as usage rises. At exactly 1,000,000 calls the bill is ₹3,00,000; one more call moves every call to ₹0.20 and the bill to ₹2,00,000.20. Customers notice cliffs like that, and pad their usage to cross them.
The other common models:
- Per unit, pay as you go: $0.40 per 1,000 messages, nothing up front. Simple and fair, but neither side can predict the bill.
- Package: ₹500 per block of 10,000 SMS, rounded up, so 25,001 messages buy 3 blocks: ₹1,500.
- Included allowance plus overage: a $99 plan includes 50,000 calls, then $1.50 per extra 1,000. At 62,000 calls the bill is $99 + 12 × $1.50 = $117. Azure’s marketplace makes you track the allowance yourself and report only the usage above it.
- Prepaid credits that usage draws down: buy ₹1,00,000 of credits and each call deducts from the balance. Checking the balance is enforcement, so it needs a fast counter, and what happens at zero (block, top up or overage) is a product decision.
- Minimum commitment: $10,000 a month committed. Usage rated at $8,500 is billed $10,000, with the $1,500 shortfall as a true-up line; usage above the commitment is billed as used.
- Per seat with true-ups: an annual contract for 100 seats at $20 a seat a month. If the customer has 130 seats with 6 months left, the true-up is 30 × $20 × 6 = $3,600.
Proration applies when the plan changes mid-period. A tenant moves from Basic at ₹3,000 a month to Pro at ₹9,000 on day 11 of a 30-day month. It gets back 3,000 × 20 ÷ 30 = ₹2,000 of unused Basic and pays 9,000 × 20 ÷ 30 = ₹6,000 for Pro: ₹4,000 due now. Usage is split at the same instant, rated at Basic prices before it and Pro prices after, which is one more reason every event carries its own occurred_at.
| Model | Bill computed as | Customer can predict it | You can forecast revenue | Watch out for |
|---|---|---|---|---|
| Per unit | Quantity × unit price | Poorly | Poorly | Bill shock |
| Graduated tiers | Each tier’s units × that tier’s price | Fairly | Fairly | Explaining tier lines |
| Volume tiers | All units × the final tier’s price | Fairly | Fairly | Cliffs where more usage costs less |
| Package | Blocks, rounded up, × block price | Well | Fairly | Rounding disputes |
| Allowance + overage | Fee + units above the allowance × rate | Well, until overage | Well | Surprise overage |
| Prepaid credits | Drawdown from a balance | Well | Cash up front | Real-time balance, expiry rules |
| Minimum commitment | The larger of commitment and rated usage | Well | Well | True-up negotiations |
| Per seat + true-up | Seats × price, reconciled later | Well | Well | Defining an “active” seat |
Enforcement is not billing
A quota check sits on the request path, so it has about a millisecond. It reads and increments a counter per tenant and meter, in memory or in Redis, or spends from a token bucket for rates (rate limiting). A hard limit rejects the request. A soft limit lets it through, warns and bills overage. A grace margin, say 5% over for an hour, avoids cutting a customer off mid-batch.
Those counters are approximate by design, which is fine for enforcement:
- Nodes often count locally and sync every few seconds. With a 5-second sync and a tenant at 400 calls a second, a quota can overshoot by up to 400 × 5 = 2,000 calls: 0.2% of a 1,000,000-call quota.
- Redis replicates asynchronously, so a failover can lose recent increments.
- If the counter store is down, the gateway fails open: requests run, uncounted.
- Counters count attempts, including requests that later fail, and reset on a schedule.
Each of those is a shrug for a quota and a dispute on an invoice, so never bill from enforcement counters. The reverse fails too: enforcing from the ledger, minutes behind, lets a tenant at 2,000 calls a second run 2,000 × 300 = 600,000 calls past its limit during a 5-minute lag. Keep the two paths separate and pull the counter up to the ledger now and then: counter = max(counter, ledger total). Sketches such as count-min are fine for spotting heavy tenants and never acceptable on an invoice.
Correctness customers can check
An invoice is only as good as your ability to explain it.
- Immutable raw events. Never update or delete usage; fix mistakes with new, negative events. Stripe works the same way: it accepts negative quantities, lets you cancel events only within 24 hours, and won’t change a finalised invoice for cancelled usage.
- Traceability. Every invoice line points at ledger rows, every ledger row at the events or rollups it summed, and every event at a source and request ID, so “why 1,840,000 tokens?” has a one-query answer.
- Reconciliation. Before invoices go out, compare metered totals with an independent source: API calls against gateway access-log counts, GB-months against daily storage snapshots, messages against the SMS provider’s own bill. Hold any tenant whose totals differ by more than, say, 0.5%.
- Replay and re-rate. Raw events in object storage and versioned price books let you recompute any period. If a pricing bug overcharged 300 tenants, re-rate them with the right version and issue credit notes; don’t edit sent invoices.
Prevent bill shock
Show usage and the estimated charge inside the product, with the time it is accurate to, and alert at 50%, 80% and 100% of an allowance or budget. A tenant who saw the bill coming rarely disputes it.
Cost per tenant
Revenue per tenant is on the invoice; cost per tenant isn’t, and in a pool it has to be estimated. AWS’s SaaS Lens describes the method: measure each tenant’s share of consumption for the resources that dominate your cloud bill, then multiply those shares by the bill. Skip the small lines; its example is that if storage is 1% of the bill, measuring each tenant’s storage may not pay off.
Suppose the shared database cluster costs $30,000 a month and tenant t_7f3a used 6% of its database time, while the compute fleet costs $50,000 and it used 4% of CPU-seconds. It costs 0.06 × $30,000 + 0.04 × $50,000 = $1,800 + $2,000 = $3,800 a month. If it pays $3,000, every invoice loses money, and the fix is pricing, a quota or an architecture change, not a better meter. The SaaS Lens shows the same at tier level: in its example the Basic tier brought in the least revenue and consumed the most infrastructure. Siloed tenants are easier, because their own accounts or tags give exact costs (tenancy models, cost-aware design).
Going deeper: failure modes and scale
Double counting after retries. Either a new ID per retry, or a dedupe window shorter than the retry horizon: a spool that replays a 3-day backlog against a 24-hour dedupe window bills it twice. Cap replay age below the window, or dedupe yourself, for longer, before events reach the provider.
Lost events when a spool fills. At 460 events a second of 200 bytes, a node spools 460 × 200 B × 3,600 ≈ 330 MB an hour, about 8 GB for a 24-hour bus outage. Size disks for the longest plausible outage, alert on spool depth, and when space runs out keep per-minute totals in memory rather than drop usage. Never block requests.
A high-cardinality dimension. Adding user_id or a raw URL path to a meter multiplies rollups by thousands and runs into caps like Stripe’s 10,000 combinations an hour. Keep dimensions to what the price list needs.
A price change mid-period. Split usage at the effective instant and rate each side with its own version. Test your provider’s edge cases: Stripe documents that after a mid-cycle price change, usage reported during the grace period reaches no invoice.
Refunds and credits. Use credit notes and negative events, and don’t let a period total go below zero; Stripe reports a negative cycle total as a quantity of 0.
Hot tenants. Partition the log and the aggregator by tenant, so one worker owns each tenant’s totals with no coordination. A tenant producing 30% of all events overloads its partition; salt its key into 8 sub-keys and sum the 8 totals at rating time, paying a merge step for the spread.
Cheat sheet
| Topic | Remember |
|---|---|
| Four jobs | Metering (exact, minutes), rating, invoicing (final after grace), enforcement (approximate, milliseconds) |
| Usage event | Tenant, meter, quantity, occurred_at, an ID minted once at the source, few dimensions |
| Emitting | Asynchronous, after the outcome; outbox for database-backed work; never block a request |
| Delivery | At least once, plus dedupe-and-add in one transaction = effectively once |
| Volume | 2B calls a day ≈ 23,000 events/s; per-minute rollups ≈ 28× fewer |
| Late events | Event time; grace period, then close; later usage → next invoice line or credit/debit note |
| Grace windows | Stripe 1 h default (up to 72 h); AWS Marketplace to 06:00 UTC on the 1st; Azure 24 h |
| Graduated vs volume | Each tier at its price vs all units at the final tier’s price; volume has cliffs |
| Enforcement | Fast counters or token buckets; never bill from them, never enforce from the ledger |
| Trust | Immutable events, traceable lines, reconciliation, replay, usage alerts |
| Cost per tenant | Consumption share × cloud bill, for the biggest lines only |
How to say it in the interview
“I separate metering, rating, invoicing and enforcement. Services emit an immutable usage event, with tenant, meter, quantity, event time and a unique ID, to a local spool, so billing can never slow down or fail a request. Events go at least once into a log keyed by tenant, and an aggregator dedupes by ID and adds to per-minute totals in one transaction, which makes the totals effectively once. At period close I wait out a grace period, rate with the price version in force and issue the invoice; anything later goes on the next invoice as an adjustment. Quotas run on fast, approximate counters at the gateway, while invoices come only from the ledger, which I reconcile against independent counts.”
How do you avoid double billing? The ID is minted once at the source and reused on every retry. The aggregator records IDs and adds quantities in the same transaction, and its dedupe window outlasts the longest replay. Reconciliation catches whatever slips through.
What happens to events that arrive after the invoice? The invoice is never edited. Usage that misses the grace period goes on the next invoice as a labelled adjustment, or into a credit or debit note against the original. If late usage is more than a sliver, lengthen the grace period or fix the producer.
How do you enforce a quota in real time? A counter per tenant and meter, in the gateway’s memory or Redis, checked and incremented on each request, with a hard, soft or grace limit. It can drift by a fraction of a percent, so it is periodically pulled up to the ledger total, and it never feeds an invoice.
Tiered or volume pricing? Graduated tiers charge each tier’s units at that tier’s price, so the marginal price falls smoothly and the bill never drops. Volume pricing charges every unit at the final tier’s price: simpler to read, but with cliffs where one more unit lowers the bill.
Where to go next
- Design usage metering and billing: the full system design.
- Control plane vs data plane: why billing stays off the request path.
- Noisy neighbours and fairness: quotas and per-tenant limits.
- Effectively-once: idempotent consumers and the inbox pattern: deduplication in depth.
- Transactional outbox: emitting usage with the work.
- Batch vs stream processing and Flink and stream processing: windows and watermarks.
- Design a rate limiter: the enforcement side.
- Design a double-entry ledger: where invoiced money goes next.
References and further reading
- Stripe Docs: Record usage for billing with the APIdocs.stripe.comMeter events, the 35-day and 5-minute timestamp window, 1,000 calls a second per account, 10,000 a second with meter event streams, dimension cardinality caps and the advice to pre-aggregate.
- Stripe API reference: Create a billing meter eventdocs.stripe.comThe identifier field, unique within a rolling period of at least 24 hours, aimed at accidental retries.
- Stripe Docs: Configure metersdocs.stripe.comSum, count and last aggregation; raw versus pre-aggregated ingestion where a newer event replaces the window; cancelling events within 24 hours and negative quantities.
- Stripe Docs: Set up tiered pricingdocs.stripe.comVolume versus graduated tiers, and how a volume total can fall as quantity rises.
- Stripe Docs: Configure an invoice finalization grace perioddocs.stripe.comA 1-hour default grace period configurable to 72 hours, late usage that reaches no invoice, and the mid-cycle price change that drops grace-period usage.
- AWS Marketplace API Reference: BatchMeterUsagedocs.aws.amazon.comIdempotent identical requests, no records 24 hours or more after the event, the previous month open until 06:00 UTC on the 1st, and 25 records per call.
- Microsoft Marketplace: Metering service APIslearn.microsoft.comOne usage event per resource, dimension and hour, a 24-hour submission window, 409 Conflict for duplicates, and sending only usage above the base fee.
- AWS Well-Architected SaaS Lens: Expenditure awarenessdocs.aws.amazon.comAttributing consumption to tenants, focusing on the biggest cost lines, and multiplying consumption shares by the bill to get cost per tenant and per tier.
- OpenMeter Docs: Usage eventsopenmeter.ioCloudEvents-based usage events, deduplication by id and source, and aggregation per time window, subject and group-by values.
- Apache Flink Docs: Timely stream processingnightlies.apache.orgEvent time versus processing time, watermarks as a declaration that earlier events should have arrived, and late elements.