Pricing in Practice/

When the Usage Bill Doesn't Match Usage: Finding the Layer That Drifted

Usage passes through several layers on its way to an invoice, and each one can lose it, count it twice or put it in the wrong period. Where each layer goes wrong, what a nightly reconciliation can and cannot catch, and why the first decision is which ledger you trust.

9 min read


A customer writes in to say their invoice is wrong. Or you notice that revenue and usage have stopped moving together: some overage was never charged, a credit balance went down twice, or a busy day seems to have fewer events than it should.

If you run usage-based billing or sell prepaid credits, you are probably keeping at least three ledgers: the usage counts in your application, the credit or allowance balance, and the subscriptions and invoices at your payment provider. Webhooks and background jobs connect them. When the numbers disagree, the question is not just which number is wrong, but at which step the ledgers split.

This post follows usage from the moment it happens to the invoice, layer by layer, with a common way each layer goes wrong. The examples use Stripe, because many teams in this situation bill through it and its documentation is specific about the edges. The same layers exist with any payment provider.

The layers

Usage passes through five steps on its way to an invoice:

  1. Capture: your application records that something billable happened.
  2. Aggregation: events are counted into a billing period.
  3. Balance: an allowance or a credit balance goes down.
  4. Sync: your records and the payment provider's are kept in step.
  5. Invoicing and corrections: the period is closed into an invoice, and later changes are applied to it.

A mismatch can start at any of them. It usually shows up at the last one.

Capture: lost and doubled events

Two things go wrong when usage is recorded: an event is never sent, or it is sent twice.

Doubling is often the result of doing the right thing. A request times out, your code retries, and the first attempt had in fact gone through. Stripe's engineering blog describes the general problem: after a network failure, a client cannot tell whether the operation succeeded. The usual answer is an idempotency key. The client creates a unique ID for each operation and sends the same ID on every retry, and the server uses it to avoid doing the work twice.

Stripe's documentation on recording usage recommends this for meter events, and each meter event carries an identifier. Its API reference adds a limit worth knowing: uniqueness of that identifier is enforced over a rolling period of at least 24 hours, aimed at accidental retries in a short window. A job that replays yesterday's events after a crash falls outside that window, so the duplicate check has to live in your own store as well, for example as a unique constraint on the event ID.

For this to work, the ID has to be created once, with the work, and stored with it, not generated again on each attempt.

Lost events are harder, because nothing arrives to tell you. If you count usage in your own database and send the event to the provider in a separate step, a crash between the two leaves usage you counted but never reported. The last section of this post comes back to that gap.

Aggregation: usage that lands in the wrong period

Once events arrive, they are counted into a billing period. The awkward cases are events that arrive late.

Stripe's documentation states that a meter event can carry a timestamp up to 35 calendar days in the past, but that accepting an event does not mean it will be billed. An event that arrives after an invoice is finalized may appear in the meter's usage summaries without updating the finalized invoice, and it is not added to the next invoice either.

The buffer between the end of a period and finalization is a grace period. Stripe's documentation on it gives a default of one hour, configurable up to 72 hours, and describes three limits:

  • Regular cycle invoices and invoices for a subscription schedule's phase change include usage reported during the grace period. Other invoices do not.
  • Threshold invoices include usage received before they were created, and nothing reported afterwards.
  • When a price changes in the middle of a cycle, usage reported during the grace period does not appear on the current invoice or on any later one.

So the same usage can be counted in your application and be missing from every invoice, with no error on either side. If you change prices mid-cycle, this is one of the first places to look.

Time zones belong to this layer too. Stripe's documentation on meters states that for pre-aggregated meters, hour and day windows follow the event timestamp in UTC, and a later event in the same window replaces an earlier one. If your application counts per local day, its totals and the provider's will differ around midnight even when no event is lost.

Balance: credits taken twice or not at all

If customers draw down an allowance or prepaid credits, the balance is a third ledger, and it is the one most exposed to concurrency. Two requests read the same balance, both decide there is enough, and both go ahead. A retry deducts again. A failed request deducts and never gives the credit back.

One developer described finding three bugs in the billing step of a gateway that sends requests to several model providers, each of which either created credits out of nothing or lost them. Another founder described an edge condition under which overage was never charged, which came to light after one customer had used far more than they paid for. A bug like these does not have to raise an error to cost money.

A separate source of disagreement is timing: Stripe's documentation notes that meter events are processed asynchronously, so usage summaries and upcoming invoices may not yet reflect events it has just received, and that it accepts one concurrent call per customer per meter. A balance you show the customer from your own records and a total read back from the provider can disagree for a while without either being wrong yet.

Sync: webhooks that arrive late, twice, out of order or not at all

The link between your records and the payment provider is usually webhooks. Stripe's webhook documentation is direct about what they do not guarantee:

  • In live mode, failed deliveries are retried with exponential backoff for up to three days.
  • Events are not guaranteed to arrive in the order they were generated. The created timestamp has one-second precision and several events can share it, so it should not be used to decide order or whether an event was already handled.
  • The same event can be delivered more than once, so the documentation advises recording the IDs of processed events and skipping repeats. In some cases two different event objects are generated for the same change.

Its documentation on managing webhook endpoints adds that in rare cases an event is not generated at all, so there is nothing to deliver and nothing to list. Its recovery step is to fetch the current state of the affected object directly.

A deploy is one way to miss one. One developer described a webhook that arrived while a deploy was in progress: the customer's access was never updated, and without a complaint it was easy to miss.

For catching up after downtime, the same page gives a procedure: list the events that failed to deliver, starting from the last one you processed successfully, and handle them one at a time. The list covers events created in the last 30 days, and processing them by hand does not stop the automatic retries, so the handler has to tolerate running twice.

Invoicing and corrections: what cannot be changed afterwards

The last layer is where a mismatch becomes something a customer can see, and where your options narrow.

  • Wrong usage. Stripe's documentation states that meter events can be cancelled if they were sent within the last 24 hours. Cancelling usage that is already on a finalized invoice does not update that invoice or produce a corrected one. You can also send a negative adjustment; if the period's total goes below zero, the invoice bills zero.
  • Refunds and credits. Its documentation on billing credits states that voiding an invoice returns the credits applied to it, but issuing a credit note does not; to give the customer those credits back, you create a new credit grant. Credits applied to an invoice cannot be voided.
  • Disputes. Its documentation on disputes states that when a customer disputes a payment, the disputed amount and a dispute fee are taken from your balance and held for the whole dispute. You usually have 7 to 21 days to respond, the card issuer usually decides within 60 to 75 days, and the whole process can take two to three months. During a dispute you cannot refund the payment outside the dispute process.

In each case the money and the usage move separately. A refund can leave consumed credits on your side; a dispute can take the money for usage you have already delivered and paid your own suppliers for. Decide in advance which ledger you correct first and how you record the link between the two.

The nightly reconciliation

A common patch is a scheduled job that compares the application's database with the payment provider and flags differences. One founder whose team runs such a job described comparing subscription status, top-ups, connected account status and invoices, and was still asking what else they should be comparing.

That question marks the limit of the approach. A reconciliation job:

  • finds differences after they happen; it does not prevent them;
  • covers the fields someone thought to list; a mismatch in a field nobody compares stays invisible;
  • can look back so far and no further: the list of undelivered events covers 30 days, and a finalized invoice will not change whatever the job finds.

It is still worth having. To get more from it, compare at each layer rather than at the end alone: events your application recorded against events the provider accepted, your balance against the sum of its movements, and your subscription state against the provider's current state rather than the last webhook you received.

Decide which ledger is right

Underneath every approach is the same question: when two ledgers disagree, which one wins? Public approaches answer it differently.

  • Your own database is the record. Write the usage and the message to be sent in one database transaction, and let a separate process send the message on. This is the transactional outbox pattern, which addresses how to update a database and send a message atomically. Your own record and what you intend to send cannot drift apart. The cost is an extra table and an extra process, and the sender can still deliver twice, so the receiving side still needs to ignore repeats.
  • The payment provider is the record. Instead of applying each webhook's contents, re-fetch the customer's full current state from the provider whenever something changes, and overwrite your copy. One developer argues for this as a way around out-of-order and missing webhooks, and Stripe's documentation also points to fetching the current object when an event is missing or out of order. The cost is an extra round trip per change, rate limits, and coverage of what the provider holds and nothing else: your usage counts and credit balances are not there.
  • Each ledger for what it alone knows. Usage and balances are authoritative on your side, payments and invoices on the provider's, and reconciliation checks the seams between them.

None of these removes the layers above. Each decides which side you trust when they split, and that makes every other fix easier to reason about: you know which side to correct, and which side the customer's invoice should follow.

Before writing the next reconciliation script, write down for each layer which ledger is authoritative, how an event is identified across retries, and what happens to usage that arrives after a period is closed. For the second, our usage-reporting guide has a short section on keeping an event's ID with a job that may run again after a crash: Choose the event ID.

This post was written by the team building ShipRev, which keeps a SaaS product's identity, subscriptions, metering and entitlements on one customer record. Those five layers exist with ShipRev too. ShipRev is not open yet: the managed cloud is built but not yet open, and the source repository is not public.

Related use case: Monthly allowances

Keep reading