Usage billing
0-day streakReview

Your progress

Level 1

0 XP100 XP to level 2
0day streak
0best streak
0topics started
0cards reviewed
    Review cards

    Progress is stored only in this browser. No account is required.

    Layer 4 · Full breakdown · 33 min · 13 figures

    Design usage metering and billing

    Design usage-based billing for a cloud API platform with 200,000 customers and 10 billion usage events a day: lossless ingestion with idempotency keys, real-time usage and prepaid balances, late events, versioned price plans, exact invoices and audit-safe corrections.

    Every WhatsApp message sent through your API, every million LLM tokens and every gigabyte left in a bucket has to be written down and, once a month, turned into a bill. That is usage metering and billing: a counter with a price list attached, which must be fast and exact at the same time. A developer who tops up ₹2,000 over UPI expects the dashboard to move within a minute and the API to stop when the money runs out. Finance needs an invoice that is a legal document — nothing counted twice, nothing lost, every line reproducible years later — even though prices change mid-month and events arrive days late. You will design it for a cloud API platform (think an Indian messaging-and-AI API company, or Twilio, the OpenAI API and AWS) with 200,000 paying customers and about 10 billion usage events a day.

    Understand the problem

    Billing is three jobs that weak answers blur together. Metering measures what each customer used. Rating turns those quantities into money using the customer’s price plan. Invoicing turns money into a numbered legal document, collects payment and records it in the books. They have different shapes: metering is a high-volume data pipeline, rating is a pure function that must give the same answer every time, and invoicing is a low-volume, high-stakes workflow. Billing is also not rate limiting: a limiter may be a few percent off, a bill may not. The concept page on metering and usage-based billing covers the vocabulary; this page designs the system.

    Functional requirements

    1. Product services should be able to report usage — customer, meter, quantity, event time, an idempotency key, and dimensions such as region, model or message category — without slowing down the request that caused it.
    2. Customers should see their usage and cost in near real time, and get alerts when they cross thresholds they set, such as 50%, 80% and 100% of a monthly budget.
    3. Prepaid balances and spend caps should be enforced. When a prepaid wallet reaches zero or a postpaid customer hits their cap, the platform blocks or degrades service within seconds.
    4. At period end, each customer should get an accurate invoice computed from versioned price plans — tiers, free allowances, commits, discounts, credits and GST — and handed to payments.
    5. Finance and support should be able to correct usage and bills — late events, mistaken events, credits, refunds and re-rating — with a complete audit trail.

    Non-functional requirements

    • Scale. 200,000 paying customers, about 40 meters, roughly 10 billion events a day, peaks of 3× average during a festive-sale campaign hour.
    • No acknowledged event is ever lost, even if a machine or zone fails.
    • Exactly-once billing effect. Duplicates will arrive; each idempotency key affects an invoice once.
    • Freshness. Dashboards and alerts reflect an event within 5 minutes at p99, about a minute typically.
    • Enforcement. A prepaid customer at zero is blocked within 10 seconds, with overshoot bounded in rupees.
    • Invoices on time. All 200,000 drafted within 2 hours and finalized within 6 hours of the grace window closing.
    • Reproducible. Any invoice line can be recomputed years later from raw events, the plan version and the code version. Keep records for the statutory retention period; plan for 8 years.
    • Exact money. Integer paise for amounts, fixed-scale decimals for unit prices, never floats.
    • Availability. Ingestion at 99.99% and never on the product’s critical path; invoicing can tolerate an hour’s outage.

    In the interview

    Ask four questions before drawing anything. “Postpaid, prepaid, or both?” — prepaid puts a balance check on the request path. “May a dashboard number change after the customer has seen it?” — if yes, you can have a fast provisional path. “What happens at zero: block, degrade or overdraft, and by how much?” And “Which pricing models and tax regimes must the first version handle?” The answers tell you whether you are building a data pipeline, a ledger, or — as usual — both.

    Out of scope

    • Payment rails. Cards, UPI Autopay mandates and bank transfers belong to the payment system. We hand it a finalized invoice and receive a result.
    • Tax determination. Which GST rate applies, place of supply, export rules. A tax engine answers; we store its answer and the rule version it used.
    • The general ledger and revenue recognition. We post balanced entries to a double-entry ledger; finance owns the books and the month-end close.
    • Per-second throttling and abuse. The rate limiter stops bursts; we deal in money.
    • Quoting and contract negotiation. Sales negotiates the deal; we receive the signed contract as configuration.

    The set-up

    Plan the approach

    Satisfy each functional requirement with the simplest design, then spend most of the time in the deep dives. Say the shape early: “an append-only usage log at the bottom; two read paths over it — a fast, provisional one for dashboards and balances and a slow, exact one for invoices; a rating engine that turns quantities into money; and a ledger that records every rupee.” The deep dives follow from that sentence: ingestion without loss or duplicates, the two paths, late and corrected data, pricing, real-time balances, month-end invoicing, and explaining a bill.

    Core entities

    • Usage event. One immutable fact: customer, meter, quantity, event time, idempotency key, producer, dimensions. Corrections are new events.
    • Meter. What is measured and how it aggregates: sum (tokens), count (API calls), max, latest (seats at period end), unique count (monthly active users) or time-weighted average (GB-months).
    • Price plan version. An immutable rate card with an effective date. Changing a price publishes a new version.
    • Contract. Which plan versions a customer is on and when, billing anchor, currency, GSTIN, commits, discounts and credits.
    • Usage total. Quantity per customer × meter × dimensions × hour (provisional) or day (closed).
    • Invoice and line items. A draft until finalized, then numbered and frozen; each line records its plan version, source totals and code version.
    • Adjustment. A credit note or next-invoice line that corrects a finalized invoice.
    • Wallet, lease and ledger entry. A prepaid balance, chunks of it reserved by gateway pods, and balanced debits and credits for every money movement.

    A meter and one price-plan version, written as configuration:

    meter:
      id: messages.whatsapp
      unit: message
      aggregation: sum          # sum | count | max | latest | unique_count | time_weighted_avg
      dimensions: [country, category]
    
    price_plan_version:
      plan: growth
      version: 7
      effective_from: 2026-10-01T00:00:00+05:30
      currency: INR
      prices:
        - meter: messages.whatsapp
          model: graduated      # each tier prices only the units that fall in it
          tiers:
            - { up_to: 100000, unit_price: "0.20" }   # rupees per message
            - { up_to: null,   unit_price: "0.15" }
        - meter: storage.gb_month
          model: per_unit
          free_allowance: 50
          unit_price: "2.00"
      rounding: { scope: line_item, to: paise, mode: half_even }

    API

    There are four surfaces: ingestion for product services, reads for customers, the balance check for gateways, and admin calls for plans and corrections.

    Ingestion. Services send batches of up to 500 events. The quantity is a decimal string, as Metronome also recommends, so no JSON parser turns it into a float. The response reports each event as accepted, duplicate or rejected.

    POST /v1/usage-events
    Content-Type: application/json
    
    {
      "events": [
        {
          "idempotency_key": "wa-gw:wamid.HBgM91987x",
          "customer_id": "cus_4821",
          "meter": "messages.whatsapp",
          "quantity": "1",
          "timestamp": "2026-10-31T23:58:41.120+05:30",
          "dimensions": { "country": "IN", "category": "utility" },
          "producer": "wa-gateway-7"
        }
      ]
    }
    { "accepted": 1, "duplicates": 0, "rejected": [] }

    Reads mark the boundary between final and provisional numbers. By 18 October this customer is past the first 1,00,000 messages, so each day is priced at the second tier’s 15 paise.

    GET /v1/customers/cus_4821/usage?meter=messages.whatsapp&from=2026-10-01&group_by=day
    {
      "currency": "INR",
      "final_through": "2026-10-18",
      "rows": [
        { "date": "2026-10-18", "quantity": "21950", "amount": "3292.50", "status": "final" },
        { "date": "2026-10-19", "quantity": "24410", "amount": "3661.50", "status": "provisional" }
      ]
    }

    Balance leases (internal, called by gateway pods, explained in the deep dives):

    POST /internal/v1/leases
    Content-Type: application/json
    
    { "customer_id": "cus_9177", "pod": "gw-41", "want": "150.00", "ttl_s": 60 }
    { "lease_id": "ls_77f", "granted": "150.00", "wallet_after": "9550.00", "expires_in_s": 60 }

    Admin, low traffic and fully audited:

    POST /v1/price-plans/growth/versions          publish a new immutable version with effective_from
    POST /v1/customers/cus_4821/plan-changes      move a customer to another version at a timestamp
    POST /v1/usage-corrections                    void events by key, or replace a customer's time range
    POST /v1/invoices/inv_2610_0042/credit-notes  credit a finalized invoice, with a reason
    GET  /v1/invoices/inv_2610_0042/lines/2/usage drill down: daily totals, then sample raw events

    Back-of-envelope estimates

    These numbers show where the hard parts are (method: back-of-envelope estimation).

    Event rate. 10 billion events ÷ 86,400 s ≈ 116k events/s on average, and 3× that, about 350k events/s, at peak.

    Bandwidth. At about 250 bytes an event: 116k × 250 B ≈ 29 MB/s, about 87 MB/s at peak. A modest Kafka cluster; throughput is not the hard part.

    Raw archive. 10B × 250 B = 2.5 TB a day. Parquet with zstd shrinks repetitive events roughly tenfold: 250 GB a day, 91 TB a year, about 730 TB over 8 years — cheap in object storage, so keep everything.

    Dedupe state. Every key for 24 hours: 10B × about 40 bytes (16-byte hash, timestamp, overhead) ≈ 400 GB, about 3 GB per task across 128 tasks — ordinary for RocksDB-backed state.

    Totals. 200,000 customers × about 15 active meter-and-dimension series ≈ 3M series. If every series changed every minute, that is 50k upserts/s of hourly totals. Closed daily rows: 3M a day, 93M a month — an invoice run reads 93 million rows, not 310 billion events.

    Balance checks. About 100k prepaid billable requests a second at peak: checking a database on each is 100k transactions/s, with big senders hammering one row. With leases (below), 150,000 pod-and-customer pairs renewing every 15 s is about 10k lease calls/s, off the request path.

    Invoice run. About 1.5 s each for query, rating, tax and PDF: 200,000 × 1.5 s = 300,000 worker-seconds, about 50 minutes on 100 workers.

    Rounding. Round each event to the paisa and each can be off by half a paisa: 10B × ₹0.005 = up to ₹5 crore a day, all one way if sub-paisa LLM calls round to zero. So round per invoice line, never per event.

    Tip

    The number that shapes this design is not events per second but how many rows an invoice reads. Closing each day into exact totals turns 310 billion monthly events into 93 million rows, which makes month-end fast, cheap and repeatable.

    High-level design

    Build one requirement at a time; by the end it works for one region at moderate scale.

    Product services report usage without losing any

    Every product service links a small metering SDK that batches events (500 or one second, whichever comes first) to an ingest service. If ingest is slow or down, the SDK spools the batch to local disk and retries later with the same idempotency keys. A billing outage should delay revenue, never stop a WhatsApp message.

    The ingest service validates each event — known customer and meter, a timestamp at most 5 minutes ahead and 35 days old, a decimal quantity — stamps the arrival time and writes it to the Kafka topic usage-raw, keyed by customer. It returns 202 only after all in-sync replicas have it. Invalid events go to a dead-letter topic with the reason; each one is revenue somebody must chase.

    An archiver writes every event, unchanged and before any business logic, to object storage as Parquet partitioned by event date. This archive is the source of truth.

    Step 1: services report usage through a spooling SDK; the ingest service validates and writes to Kafka; an archiver keeps every event.Notice The archive sits before any deduplication or pricing logic, so a bug anywhere downstream can be fixed by replaying it.

    Customers see near-real-time usage, cost and alerts

    A stream job (for example on Flink) reads usage-raw, drops duplicates and adds each quantity into a total per customer × meter × dimensions × hour, upserting changed totals into a usage store every 60 seconds. The usage API prices them with the same rating library the invoices use, so the customer sees “₹39,672.50 so far” rather than “2,31,150 messages”.

    An alert evaluator recomputes month-to-date cost for customers whose totals moved. Each threshold fires once per period, and if several are crossed at once only the highest is sent, as Orb’s alerting does. A notifier sends email, SMS or a signed webhook (see the webhook delivery service).

    Step 2: a stream job keeps hourly totals per customer and meter; the usage API prices them and alerts fire on thresholds.Notice These numbers are provisional: fresh within a minute, but allowed to move slightly when the day is closed exactly.

    Prepaid balances and spend caps are enforced

    Most self-serve developers pay in advance: top up ₹2,000 over UPI, spend it, top up again. The simplest design puts a balance service behind the API gateway. Before each billable call the gateway asks it to debit the call’s price from the wallet, and it answers allow or deny. Top-ups arrive from payments as credits, and every movement is journalled to the ledger asynchronously.

    A postpaid spend cap works the same way, with the cap minus month-to-date spend as the budget. A denied call gets 402 with a code such as balance_exhausted, so client code can tell “top up” from “slow down”.

    Step 3: the gateway checks a balance service before each billable call; top-ups credit the wallet and every movement is journalled.Notice Every billable request now waits on the balance service — correct, but a synchronous dependency we will remove in the deep dives.

    Invoices are generated from versioned price plans

    Invoices must not depend on provisional totals. A daily close runs for each day once its late events have arrived: it reads that day’s archive partition, removes duplicates exactly, applies corrections and writes closed daily totals.

    When a period ends and its 48-hour grace window passes, a scheduler enqueues one invoice job per customer. A worker loads the contract and the plan versions in force from the price catalog, reads the closed daily totals, rates each meter, applies commits, discounts and credits, gets GST from the tax engine, rounds each line and writes a draft. Finalizing numbers and freezes it, renders the PDF, posts to the ledger and hands it to payments.

    Step 4: invoices are built from closed daily totals, not from the live stream, and priced with the plan versions in force during the period.Notice The invoice path never reads the 10 billion raw events of the month; it reads about 3 million closed rows a day.

    Support and finance correct usage and bills with an audit trail

    Corrections are just more events. A corrections API accepts three kinds — void events by key, replace a customer’s usage for a time range, or grant a credit — and records each, with author, reason and ticket, in a usage-corrections topic and the archive. Nothing is deleted. If the period is still open, the affected days re-close and the draft is recomputed; if it is finalized, the difference becomes a credit note or a next-invoice adjustment.

    You now have a working design. The deep dives are about what breaks at 350,000 events a second, with real retries, real lateness and real money.

    Deep dives

    How do we ingest billions of events without losing or double-counting any?

    A lost event is lost revenue; a double-counted one overcharges a customer. Retrying fixes the first and causes the second, so: retry everything, and make duplicates harmless.

    Bad: write usage straight into the billing database. Each API call runs UPDATE usage SET qty = qty + 1 WHERE …. You gain simplicity. You pay everywhere else: a timeout after the commit means a retry and a double count, a crash before it loses the call, a bulk sender at 2,000 messages a second queues on one row lock, and the billing database sits on every product request. When a customer asks “which two million calls?”, you have a number, not events.

    Good: a durable log plus idempotency keys. Producers publish to Kafka with an idempotency key; consumers drop keys they have seen. At-least-once delivery plus an idempotent consumer gives an exactly-once effect (effectively-once consumers). The key must be deterministic — derived from the business fact, such as the WhatsApp message ID or the LLM request ID (idempotency). For heartbeats like “this VM ran this minute”, Metronome’s guide builds the ID from node ID and minute and sends each heartbeat twice: duplicates are dropped, gaps are not. A UUID generated at send time defeats the purpose, because a retry gets a new one.

    How long must you remember a key? Billing vendors draw the line in different places:

    Stripe meter events (v1) Orb Metronome This design
    Duplicate-key window identifier unique over a rolling period of at least 24 h idempotency_key ingested at most once transaction_id: 34 days 24 h in the stream; the whole day, exactly, in the daily close
    Oldest event accepted 35 days Account grace period (12 h by default) 34-day ingest window 35 days; older goes to review
    Furthest in the future 5 minutes 5 minutes 24 hours 5 minutes
    Fixing bad usage Cancel within 24 h, or send negative quantities Amend, deprecate or backfill; old events archived Replay within 34 days; operations team beyond Void or replace events; credit notes after finalization
    How three billing vendors bound duplicates and lateness, and what this design chooses

    Read the Stripe column carefully: events up to 35 days old are accepted, but key uniqueness is promised for at least 24 hours. A producer replaying two days of spooled events cannot rely on the vendor to catch duplicates. Make the dedupe window as long as the oldest event you accept, or dedupe again later.

    Great: layered defences, each cheap at its own layer.

    • Spool at the producer. A pod emitting 500 events a second at 250 bytes needs 450 MB of disk to ride out an hour of ingest outage.
    • Acknowledge only after replication: acks=all, three replicas, at least two in sync. Kafka’s idempotent producer removes its own retry duplicates; ad click aggregation walks through every hop where copies appear.
    • Archive raw, duplicates included, so a dedupe bug is fixed by replaying, not guessing.
    • Dedupe twice. The stream job keeps keyed state per (producer, key) with a 24-hour TTL, storing a 128-bit hash: the chance of any collision among 10 billion keys is about 10²⁰ ÷ 2¹²⁹ ≈ 10⁻¹⁹. That catches retries, which come within minutes. The daily close then groups the whole day’s partition by key. A duplicate carries the original’s event time, so both copies land in the same partition; a key reused with a different event time is a producer bug, which a nightly cross-day key check reports.
    • Reconcile with producers. Each producer emits an hourly marker: “wa-gateway-7 sent 1,84,220 events, quantity 1,84,220, for 10:00–11:00”. The close compares it with the archive; a shortfall triggers an alert and a spool replay. Without markers, a dropped event is invisible.
    • Pre-aggregate where allowed. A gateway serving 80,000 calls a second can emit one event per customer × route class × minute, keyed by (producer, customer, meter, minute); Stripe’s docs suggest this to stay under its 1,000 calls/s meter-event limit. You gain 10–100× fewer events and pay with drill-down that stops at the minute — so keep per-event records for often-disputed meters such as messages.
    One WhatsApp send reported three times: the stream’s 24-hour dedupe catches the quick retry; the daily close catches the late replay.Notice The stream briefly over-counts the 30-hour replay. The provisional number is wrong for a day; the invoice never is.

    One more trap: a customer producing 15% of all events makes one partition and one task hot. Salt their key — cus_4821#0 to #7, chosen by hashing the idempotency key so copies share a salt — and merge the partial totals downstream.

    How do we show real-time usage and still produce exact invoices?

    The dashboard needs an answer within a minute; the invoice needs the answer. The mistake is answering them with different code.

    Bad: compute usage on demand from raw events. You gain one consistent number and pay with page loads that scan hundreds of millions of rows for a big sender.

    Good: streaming totals for dashboards, a separate batch job for invoices. Fresh and exact. You pay with two implementations of “what counts” that drift apart, until a customer emails a screenshot: “your dashboard said ₹9,800, your invoice says ₹10,200”.

    Great: one definition, two schedules, and a visible boundary. Compile the same meter definitions and rating library into the stream job and the daily close. The close for day D runs at D + 2 days, once producer markers are in. The usage API returns closed days plus provisional hours since, and says so: “final through 18 October”. Invoices use closed days only. Compare each day’s provisional and closed totals; drift above 0.1% means dedupe or lateness is misbehaving, and a monitor should say so before a customer does.

    The same meter definitions and rating code run live every minute and again, exactly, two days later; the dashboard stitches the two together.Notice The boundary on the chart. Everything left of “final through” is what the invoice will say; everything right of it is still allowed to move.

    Not every meter is a sum, and the aggregation decides how the two paths work:

    • Sum, count, min and max are decomposable: hours combine into days and days into months.
    • Latest (seats at month end) keeps the newest event time, so a late event cannot overwrite a newer value.
    • Time-weighted average bills storage fairly: 100 GB for 10 days then 400 GB for 20 days of a 30-day month is (100 × 10 + 400 × 20) ÷ 30 = 300 GB-months; latest or max would charge for 400.
    • Unique count (monthly active users) is not decomposable: Monday’s and Tuesday’s users do not add up. Orb draws the same line — SUM and COUNT are evaluated in real time, COUNT DISTINCT needs a periodic scan. A HyperLogLog sketch with 16,384 registers has a standard error of 1.04 ÷ √16,384 ≈ 0.8% — fine for a chart. The invoice uses the exact set: 2 million user IDs × 16 bytes is only 32 MB. Never bill on an estimate unless the contract says so.

    How do we handle late, out-of-order and corrected events?

    Every event has two times: when usage happened and when it arrived. Bill by the first — it picks the period and the plan version. Arrival time only tells the pipeline how patient to be (see batch vs stream processing).

    Lateness is ordinary: a spool replaying after an outage, a storage snapshot taken at 00:30 and reported at 01:00, a partner sending delivery receipts in hourly files. Bound it at the door: reject anything more than 5 minutes in the future (Stripe and Orb use the same limit for clock skew) and send anything older than 35 days to review.

    Bad: invoice at midnight with whatever has arrived. Usage from 23:58 on 31 October that arrives at 00:20 is lost, or lands on November’s invoice at November’s prices.

    Good: a grace window, then finalize. Keep the invoice a draft for a while and keep adding usage. Stripe’s default grace period is one hour, configurable up to 72; Orb’s is 12 hours by default. Choose 48 hours, because nightly reporters and spool replays need more than an hour. The real question is what happens after the window. Stripe accepts such events into meter summaries but adds them to neither the finalized invoice nor the next one — unbilled usage, and summaries that disagree with invoices.

    Great: grace, finalize, then adjust forward — never edit. Late usage for a finalized period is rated at that period’s plan version and billed on the next invoice as its own line: “October usage reported late: 12,400 messages”. And close early when every producer’s marker has passed the period end.

    Three October events and when they arrived: two within the 48-hour grace window, one after the invoice was finalized.Notice Event C is still billed, at October’s prices, but as a new line on the next invoice. The October invoice itself never changes.

    Inside the stream job, hourly buckets are keyed by event time and accept updates for 48 hours; anything later goes to a side output that feeds the late-adjustment path.

    Corrections come in three kinds:

    • Void events by idempotency key — say, 50 test messages sent under a live customer.
    • Replace a range with a corrected set for one customer, meter and time span. Old events are marked superseded, never deleted; Orb’s backfill API archives them the same way.
    • Commercial credit, such as ₹5,000 for an outage: money changes, usage does not.

    What a correction does depends on the invoice’s state:

    The life of an invoice, and where corrections land before and after finalization.Notice The arrows only go forwards. Before finalization a correction recomputes the draft; after it, a correction creates a new document that points back.

    After finalization, the system re-rates the period with corrected data, diffs it against the issued invoice line by line, and issues a credit note for overbilling or an adjustment for underbilling. Decide in advance whether to chase small or old underbilling — say, nothing under ₹100 or older than a quarter — and write it into the terms.

    Never edit a finalized invoice

    Once an invoice has a number and the customer has it, it is a record that tax authorities, auditors and the customer’s finance team all rely on. A silent edit makes your copy disagree with theirs. Every change after finalization is a new, numbered document — a credit note or a debit — that references the original and states why.

    How do we price usage flexibly and reproducibly?

    Rating is a function: quantities in, money out. It must be deterministic — same events, plan version and code version, same rupees, even in six years — and flexible enough for whatever sales invents next.

    Bad: prices in code or mutable rows. Fast on day one. Then the price changes on 15 November, an October invoice regenerated for a dispute uses November’s price, and nobody can say what the customer was promised.

    Good: versioned, immutable price plans. Every change publishes a new version with an effective_from, and the engine picks the version in force at each event’s time, so late October usage is still priced at October rates. You gain reproducibility; you pay with version sprawl, since enterprise deals become versions of their own.

    Great: a rating pipeline of explicit, ordered, pure stages, with each invoice recording the version of every input it used.

    The rating pipeline, followed for one customer’s October invoice.Notice Where rounding happens: once per line, after pricing at full precision — never per event.

    The whole invoice, so you can check the arithmetic:

    Line Quantity Rule Amount
    Platform fee 1 month flat ₹5,000.00
    WhatsApp and SMS messages 4,00,000 1,00,000 × ₹0.20 + 3,00,000 × ₹0.15 ₹65,000.00
    LLM input tokens 120M ₹40 per million ₹4,800.00
    LLM output tokens 25M ₹160 per million ₹4,000.00
    Storage 300 GB-months first 50 free, then ₹2 each ₹500.00
    Promotion: 10% off messaging −10% × ₹65,000 −₹6,500.00
    Taxable value ₹72,800.00
    GST at 18% IGST, or CGST 9% + SGST 9% within the state ₹13,104.00
    Total ₹85,904.00

    Graduated versus volume tiers. The message line uses graduated tiers: each tier prices only the units inside it. Under volume tiers, the tier your total lands in prices every unit — and, as Stripe’s docs point out, the bill can go down as usage goes up. 1,00,000 messages cost ₹20,000 either way; one more moves every unit to 15 paise under volume pricing, and the bill drops to ₹15,000.15.

    The same two tiers priced two ways: 20 paise for the first 1,00,000 messages, 15 paise after that.Notice The cliff: under volume pricing, message number 1,00,001 cuts the bill by about ₹5,000. Customers who notice will game it.

    Use graduated tiers for usage; keep volume tiers for quantities agreed in advance, such as seats or reserved capacity.

    The rest of the toolbox fits the same pipeline:

    • Packages (“₹500 per 10,000 OTPs”) are a ceiling division; free allowances subtract before tiering.
    • Prepaid credits burn soonest-expiring first, each burn a ledger entry, so “where did my ₹10,000 go?” has an answer.
    • Commits draw down as usage is rated; a shortfall at term end becomes a true-up line.
    • Mid-cycle plan changes split the period. Flat fees prorate by time — a ₹5,000 to ₹15,000 upgrade on day 16 of 30 costs 15 ÷ 30 × ₹5,000 + 15 ÷ 30 × ₹15,000 = ₹10,000 — and the contract says whether tier counts restart. It is a hard edge: Stripe’s docs warn that a mid-cycle price change drops usage reported during the grace period.
    • Currency is fixed per billing account, with a price book per currency rather than conversion at rating time.

    Money is not a float

    0.1 + 0.2 is not 0.3 in binary floating point, and over 10 billion events the errors add up. Store amounts as integer paise and prices and quantities as fixed-scale decimals (₹0.00004 per token needs at least six places), and carry numbers as JSON strings — Metronome asks for exactly this and computes with arbitrary-precision decimals. Round once per line with a documented rule such as half-even, and make the total the sum of rounded lines, so every printed number adds up.

    How do we enforce prepaid balances and spend caps in real time?

    A prepaid customer with ₹10,000 sends messages at ₹0.20 each. You want two things that pull against each other: never let them run far below zero, and never add latency to their sends.

    Bad: check the ledger on every request. Overshoot is zero, but at 100,000 prepaid requests a second the wallet store becomes the hottest database in the company, a bulk sender serialises on one row, and a slow ledger means slow messages.

    Good: deduct in batches from metered usage. Every 5 minutes, subtract rated usage and block empty wallets. The request path is untouched; you pay in overshoot ≈ lag × burn rate. A sender spending ₹100 a second (500 messages × ₹0.20), with 5-minute batches and a minute of pipeline lag, goes 360 s × ₹100 = ₹36,000 past zero. Streaming totals cut that to about 75 s × ₹100 = ₹7,500 — still enough for a fraudster with a stolen card to burn money you never collect.

    Great: lease pre-debited chunks to gateway pods — the token-leasing idea from the rate limiter, applied to money. A pod asks the customer’s balance shard for a lease of about 30 seconds of its spend, and the shard subtracts it from the wallet before granting it. The pod debits each request from the lease in memory, renews at half used, and unspent money returns when the 60-second lease expires.

    Top: a balance shard leases pre-debited chunks to gateway pods. Bottom: how far past zero a ₹100-a-second sender gets under each approach.Notice Leases trade overshoot for stranded balance: the wallet can never go negative, but a few seconds of money can sit unused in other pods’ leases.

    You gain a wallet that never goes negative, no network call on the request path, and about 10,000 lease calls a second instead of 100,000 checks. You pay with stranded balance: spread over 20 pods, the ₹100-a-second sender has about ₹150 per pod (30 s × ₹5), so up to ₹3,000 can sit in other pods’ leases while one pod runs dry. Shrink leases towards single requests as the wallet nears zero.

    An LLM call’s price is unknown until it finishes. Reserve the worst case — input tokens plus max_tokens at the output price — then settle the actual cost and release the rest.

    Three more pieces:

    • Thresholds and top-ups. “Below ₹1,000” and “depleted” events feed alerts and opt-in auto-recharge from a UPI Autopay mandate or card. Like Orb’s balance alerts, fire on the transition, not on every check.
    • An explicit overdraft, such as ₹500, for customers who prefer debt to an outage mid-campaign: the last lease may go negative by that much, recovered from the next top-up.
    • Reconciliation. Leases debit at a unit price; the close rates exactly. Where tiers or discounts make them differ, the close posts the difference to the wallet.

    Postpaid spend caps reuse the machinery with the cap minus month-to-date spend as the budget; at zero, service blocks, alerts or degrades, as the customer chose.

    Check the ledger per request Batch deduction from metered usage Pre-debited leases
    Added request latency One database transaction None None (local memory)
    Load on the balance store About 100k transactions/s, hot rows A few writes per customer per interval About 10k lease calls/s, off the request path
    Overshoot past zero None Lag × burn rate: ₹36,000 in the example None from leases; an explicit overdraft if the plan allows
    Main cost A database on every request Losses on fast spenders and fraud Stranded balance near zero; more moving parts
    Use it for Very low volume, high unit price Postpaid soft caps and alerts Prepaid wallets and hard spend caps
    Enforcing a prepaid balance

    In the interview

    State the overshoot as a number: “With 5-minute batch deduction, a ₹100-a-second customer can overshoot by ₹36,000. With pre-debited leases the wallet never goes negative; the cost is up to 30 seconds of spend stranded in other pods’ leases, which we shrink near zero.”

    How do we generate 200,000 invoices reliably at month end?

    Invoicing is a batch job that must be correct first and reasonably fast second.

    Bad: one big script at midnight on the 1st. Simple, until it crashes at customer 1,23,456: restart and some customers get two invoices; skip ahead and some get none.

    Good: an idempotent job per (customer, period). The draft invoice has a unique constraint on (customer_id, period_start, period_end), so a retried job recomputes the existing draft instead of creating another. Stateless workers pull jobs from a queue — about 50 minutes for all 200,000 on 100 workers — and one customer’s failure, such as a missing GSTIN, parks only that job (the pattern from long-running jobs).

    Great: spread the load, finalize carefully, and prove the totals.

    Anniversary billing. With calendar months, every invoice, auto-debit and support question lands on the same day. Starting each customer’s period on their signup day spreads the work.

    When 200,000 invoices are generated, by billing anchor. Both panels use the same vertical scale.Notice The trade: anniversary billing turns a monthly storm into a daily trickle, but calendar months are what most business customers reconcile against.

    That is 2,00,000 ÷ 31 ≈ 6,450 invoices a day, with a bump on the last day of short months. The price: businesses close their books and file GST returns by calendar month, and a 17 October–16 November invoice is harder to reconcile. A common compromise: anniversary billing for self-serve, calendar billing for the few thousand enterprise contracts.

    Finalize in one transaction that takes the next number from the series, marks the invoice final and writes a transactional outbox record that drives the PDF, email, ledger posting and payment request idempotently. Indian GST rules, like many tax regimes, expect consecutive invoice numbers within a financial year (confirm the details with your tax team), so allocate the number only at finalization — a discarded draft must never burn one. A counter row handles hundreds of increments a second, so 2,00,000 numbers take minutes. Withdrawn invoices are voided with a reason and keep their number.

    Post to the ledger and collect. Every money movement becomes balanced entries in the double-entry ledger. Notice that a prepaid balance is a liability: you owe the customer service until they use it.

    Event Debit Credit Amount
    Prepaid top-up over UPI (tax on the top-up left out for clarity) Bank clearing Customer prepaid balance ₹2,000
    Prepaid usage burned in a day Customer prepaid balance Usage revenue ₹1,450
    October invoice finalized Accounts receivable ₹85,904 Usage revenue ₹72,800 · GST payable ₹13,104 ₹85,904
    Auto-debit succeeds Bank clearing Accounts receivable ₹85,904
    Credit note for a double count Revenue adjustments ₹10,000 · GST payable ₹1,800 Accounts receivable ₹11,800

    Collection belongs to the payment system: UPI Autopay or card mandates for self-serve, bank transfer to a per-customer virtual account for enterprises. Billing owns dunning — retry on days 1, 3 and 7, then restrict — a multi-day flow that suits a workflow engine.

    Prove the totals before finalizing:

    • Invoice line quantities equal closed daily totals, which equal the deduplicated archive counts and the producers’ markers.
    • Ledger receivables equal invoices minus payments minus credit notes, and gateway settlements match bank clearing.

    A mismatch blocks only the affected customers, not the whole run.

    How do we explain a bill?

    The most common billing ticket is “we didn’t make two million calls”. Answer it in minutes, with evidence, without an engineer.

    That works only if dashboard, invoice and drill-down share one source. Every invoice line stores its provenance: the closed totals it summed and the plan, rating-code and tax-rule versions. The drill-down walks that chain — line, daily totals by API key, region or model, hourly totals, then raw events. The archive is partitioned by event date and customer bucket, so a query engine reads a handful of files to show one customer’s hour, and can export the full list as CSV.

    Most disputes end at the breakdown: 1.6 million of the two million calls came from a forgotten staging key running a test loop. The rest end with a correction and a credit note.

    The final design

    Put the pieces together. The request path touches only the gateway and its in-memory leases; everything that decides money runs on closed, reproducible data.

    The complete metering and billing system for one region.Notice Only step 4 sits on the customer’s request path, and it never waits on a network call. Everything that decides an invoice reads closed, reproducible data.

    Follow one WhatsApp message from send to settled invoice:

    1. Report. The messaging API hands a usage event keyed by the message ID to its SDK, which batches it to the ingest service (or spools it); ingest validates it and writes it to usage-raw.
    2. Archive. Every event, duplicates included, is copied into Parquet in object storage by event date.
    3. Aggregate. The stream job drops keys seen in the last 24 hours and updates hourly totals every minute, feeding dashboards, alerts and wallet reconciliation.
    4. Enforce. For prepaid customers the gateway pod debits ₹0.20 from its local lease before forwarding, renewing in the background and refusing with 402 only when the wallet is empty.
    5. Close. Two days after each day, the close deduplicates its archive partition exactly, applies corrections, checks producer markers and writes closed totals.
    6. Invoice. After the 48-hour grace window, one idempotent job per customer rates closed totals with the plan versions in force, applies discounts and credits, adds GST and writes a draft; finalization numbers and freezes it.
    7. Collect and record. The outbox drives PDF, email, ledger entries and auto-debit; dunning handles failures.
    8. Correct. Voids, replacements and credits enter as new events: before finalization they re-close days and recompute drafts; afterwards they produce credit notes or adjustments.

    Check it against the requirements: replicated acknowledgements, producer spools and a raw archive mean no lost events; deterministic keys plus two layers of dedupe, checked against producer counts, give the exactly-once effect; minute-level totals beat the 5-minute freshness target; pre-debited leases keep wallets at or above zero with no call on the request path; 200,000 drafts take about 50 minutes; and every line points to immutable totals, plan and code versions over a 730 TB, 8-year archive, priced in integer paise.

    What interviewers look for by level

    Area Mid-level Senior Staff
    Requirements Count usage and send a bill Separates metering, rating and invoicing; asks prepaid vs postpaid and what happens at zero Treats the invoice as a legal record; sets freshness, overshoot and reproducibility targets in numbers
    Ingestion Services write to a queue Deterministic idempotency keys, at-least-once plus dedupe, a raw archive Producer spools, dedupe windows matched to accepted lateness, producer count markers, pre-aggregation trade-offs
    Real-time vs exact One pipeline for everything Streaming totals for dashboards, batch for invoices One definition run on two schedules; “final through” boundary; drift monitoring; non-decomposable meters
    Late data and corrections Not discussed Event time, a grace window before finalizing Adjust forward with credit notes; supersede, never delete; re-rating diffs; written underbilling policy
    Pricing and money Unit price × quantity Versioned plans, graduated vs volume tiers Commits, credits, proration, currencies; integer minor units; rounding per line; GST split
    Balance enforcement Check a database per request Batch deduction with an alert Pre-debited leases, overshoot stated in rupees, reservations for unknown costs, explicit overdraft
    Invoicing at scale A nightly script Idempotent job per customer and period Anniversary vs calendar trade-off, gapless numbering at finalization, ledger entries, reconciliation gates

    Follow-up questions

    What if a customer disputes an invoice, saying “we didn’t make 2 million calls”?

    Open the line’s drill-down and break the calls down by API key, region and day; most disputes end there, with a forgotten test loop or a leaked key. If not, export the raw events for the disputed days — timestamps, request IDs, source IPs — so the customer can match them against their logs. A genuine error gets a usage correction and a credit note referencing the original invoice. Track disputes by root cause: many “forgotten staging key” disputes mean you should ship per-key spend alerts.

    What if a price change takes effect mid-month for some customers?

    Publish a new plan version with effective_from at the change time and move only the affected contracts. Their periods split at the change: earlier events use the old version, later ones the new; flat fees prorate, and the contract says whether tier counts restart. The invoice shows two labelled lines, and regenerating it a year later gives the same result. Beforehand, dry-run last month under both versions to show finance the impact and catch customers whose bills would jump.

    What if a producer bug double-reported usage for three days?

    Reconciliation should catch it: archived counts double the producer’s markers, or, if the markers were wrong too, a per-customer anomaly alert fires. Fix the producer, then submit range replacements built from a trusted source such as the carrier’s delivery records; old events are superseded, not deleted. Open periods re-close; finalized ones are re-rated and credited with a reason naming the incident — and tell customers before they notice.

    What if an enterprise customer has an annual commit with rollover and a true-up?

    Model the commit as a contract object with its own balance: “₹1.2 crore of usage from 1 April to 31 March; up to ₹10 lakh unused rolls over”. Monthly invoices rate usage normally and draw it down, showing what remains; usage beyond the commit is overage at the contract rate. At term end a true-up job bills any shortfall and turns the rollover into a credit grant with its own expiry. Every draw-down, rollover and true-up is a ledger entry.

    What if we bill Indian customers in rupees with GST and export customers in dollars?

    Fix the currency per billing account and keep a price book per currency, so prices stay round. The tax engine decides from location and GSTIN: CGST plus SGST within a state, IGST across states, and possibly zero-rating for exports — and the invoice records the rule version used. When converting a dollar invoice for your own books, record the rate, its source and timestamp. Keep a separate invoice-number series per legal entity.

    What if we should buy this instead of building it?

    Buy if your pricing is ordinary and your volume fits. Stripe’s meter events allow 1,000 calls a second per account, or 10,000 events a second with v2 streams; Metronome documents up to 110,000; Orb and the open-source Lago are alternatives. At 116,000 a second on average and 350,000 at peak, you would pre-aggregate per customer, meter and minute and keep raw events yourself for disputes. Build when billing is part of the product: prepaid enforcement on the request path, unusual meters, local invoice rules. Many companies split it: own metering, the archive and balances; send closed totals to a vendor for invoicing and dunning.

    How to say it in the interview

    “I’m designing usage billing for an API platform: 200,000 customers, about 10 billion usage events a day, prepaid and postpaid. The invoice is a legal document, so the rule is: nothing lost, nothing counted twice, everything reproducible.

    The usage event is the source of truth. Each service emits immutable events with a deterministic idempotency key — the message ID, the request ID — through an SDK that spools to disk, so billing never blocks the product. An ingest service validates them and writes to Kafka, and an archiver copies every event, untouched, to object storage. That archive is what I rebuild from when anything goes wrong.

    There are two read paths with one definition. A Flink job drops duplicates with a 24-hour keyed state and keeps hourly totals per customer and meter, so dashboards and alerts are a minute behind. Two days after each day ends, a daily close recomputes that day from the archive with the same code, deduplicating exactly and checking producer counts. Invoices only ever read closed days.

    Prices are versioned and immutable, picked by event time. Rating is a pure pipeline: quantities, plan version, tiers, discounts and credits, then one rounding per line in integer paise, then GST.

    Prepaid wallets use leases: each gateway pod holds a pre-debited chunk of about 30 seconds of spend and debits locally, so the wallet never goes negative and nothing waits on a network call. Batch deduction instead would let a ₹100-a-second sender overshoot by ₹36,000.

    At period end, after a 48-hour grace window, one idempotent job per customer drafts the invoice — about 50 minutes for all 200,000 on 100 workers — then finalizes it in one transaction with a gapless number, posts ledger entries and hands it to payments. Late usage and corrections after that become adjustment lines or credit notes, never edits.”

    Concepts used in this design

    References and further reading

    1. Stripe Docs: Record usage for billing with the APIdocs.stripe.comMeter-event timestamps must be within the past 35 days and at most 5 minutes ahead; 1,000 calls/s per account, 10,000 events/s with v2 meter event streams; pre-aggregate if you need more; per-hour and per-customer dimension limits.
    2. Stripe API reference: Create a billing meter eventdocs.stripe.comThe optional identifier field, whose uniqueness Stripe enforces over a rolling period of at least 24 hours, aimed at accidental retries.
    3. Stripe Docs: Create and configure a meterdocs.stripe.comSum, count and last aggregation; raw versus pre-aggregated ingestion; cancelling events only within 24 hours; negative quantities as corrections; finalized invoices are not re-issued.
    4. Stripe Docs: Configure an invoice finalization grace perioddocs.stripe.comA default 1-hour draft-to-finalized grace period, configurable up to 72 hours; usage arriving later is not added to the finalized or the next invoice; mid-cycle price changes drop grace-period usage.
    5. Stripe Docs: Set up tiered pricingdocs.stripe.comVolume versus graduated tiers with worked examples, and the note that under volume pricing the total can go down as usage goes up.
    6. Orb API reference: Ingest eventsdocs.withorb.comEvent shape with a client-generated idempotency_key ingested at most once; timestamps older than the account grace period or more than 5 minutes ahead are rejected; 4xx versus 5xx retry rules.
    7. Orb Docs: Backfill and amend eventsdocs.withorb.comA 12-hour default reporting grace period before invoices finalize; amendments and time-range backfills archive old events instead of deleting them.
    8. Orb Docs: Alerting overviewdocs.withorb.comSpend and balance alerts; decomposable SUM and COUNT metrics evaluated in real time versus COUNT DISTINCT needing periodic full scans; only the highest crossed threshold is sent.
    9. Metronome Docs: Send usage events to /ingestdocs.metronome.comtransaction_id deduplication for 34 days, deterministic IDs for heartbeat events (node ID plus minute), numbers sent as strings and arbitrary-precision decimals, queue-and-retry with a DLQ for 4xx.
    10. Metronome Docs: Usage events at scaledocs.metronome.comUp to 110,000 events/s with a 5,000 events/s default, batches of 100, and a 34-day ingest and dedupe window used to replay traffic and re-rate drafts.