Two things have to be true about billing. A request from an account with no credit must not reach a provider, and a request that did reach one must be debited.
The obvious implementation puts both on the request path: check the balance before dispatch, write the debit after. Two synchronous database round trips inside every routed request — against a 5% routing budget that exists to keep exactly that off the path, and putting the router back in the business of knowing about money, which is a separation it was deliberately given.
Where each half went instead
The gate moved to authentication. /api/internal/authenticate already runs
on every paid request, already does one indexed lookup to validate the key, and
already returns a non-2xx that the edge turns into a refusal. It now also reads
the cached balance and answers 402 when there is none. No extra hop, and the
router still knows nothing about balances.
Metering became a reconciler. The router writes a decision record per model
attempt because BR-log-every-decision says it must, for reasons that have
nothing to do with billing. So the billing data already exists. A loop reads
that log and posts one debit per billable attempt, keyed so that re-reading the
whole log is a no-op.
Nothing is on the request path. The debit happens seconds later.
The consequence we accept
Metering runs after the fact, so an account can cross zero while requests are already in flight. Those complete and are billed, and the balance ends slightly negative.
That is stated in PDR-0006 as an accepted consequence rather than found later in a support ticket, which is the only part of this worth arguing about. The alternatives are worse:
- Reserve before dispatch. Requires pricing a response nobody has generated yet, and erring cautiously means refusing requests the customer could afford.
- Synchronous debit. Puts the write back on the path and still does not fix it, because the debit lands after the provider call either way.
The bound is small: the requests in flight at the moment of crossing. For a single caller that is one; for a fan-out it is their concurrency. Both are recoverable, neither is a surprise, and the ledger records exactly what happened.
The gate is described in its own code as "a floor, not a forecast: it refuses once the balance is gone, and does not try to predict whether this request would exhaust it". Predicting means pricing a response that does not exist.
Idempotency you can re-run
The reconciler re-reads records it has already billed on every pass. That is fine because each debit carries a dedupe key derived from the record, and the ledger has a unique index on it:
CREATE UNIQUE INDEX ledger_dedupe_idx ON ledger (dedupe_key)
WHERE dedupe_key IS NOT NULL;
Posting an existing key returns False rather than raising. Not an error — that
is how the reconciler stays idempotent, and how a replayed Stripe webhook
changes nothing. The property comes from a constraint in the database, not from
the reconciler remembering where it got to.
Which means the log can be replayed from the beginning after an outage and the ledger is unchanged. That is worth more than a cursor, because a cursor is a second piece of state that can be wrong.
The silent failure it introduces
Asynchronous metering has one bad mode: the reconciler stops and nothing notices. Requests are served, providers are paid, and nobody is billed.
Nothing errors. Traffic looks healthy. The only symptom is revenue that quietly does not arrive.
So /api/health reports seconds_since_last_debit, and the twice-daily
watchdog flags it as stalled when there has been billable traffic and no debit.
The check has to be conditional, because the reconciler only writes when there
is something to bill — silence is not automatically wrong, it is only wrong when
there was work.
That is the general shape of moving something off the request path: you trade a loud, immediate failure for a quiet, delayed one, and you owe the system a monitor in exchange.