A request counter explains nothing
Gateway dashboards show how many requests arrived. That number answers nothing: it does not say whether the spike was real traffic or one partner retrying its own 429 in a loop, whether the cache is earning its keep, or why the upstream is quiet: because nobody is calling it, or because the breaker is holding the door shut.
Toll is built around one question: where did the requests go? Every stage of the pipeline reports how many it stopped and how many it let through, and the numbers add up from top to bottom: arrived, minus no key, minus quota, minus served by cache, minus the breaker, reached the upstream.
What is inside
Six middlewares in a fixed order
Route, key, quota, cache, breaker, proxy. The order lives in one file and it is the product: each stage either answers itself or passes the request on, and the response carries a header naming the stage that served it.
A token bucket under a row lock
Refill and spend happen in a single UPDATE ... RETURNING under a row lock, with the clock taken from the database rather than any one app server. A burst of sixty parallel requests is cut exactly at the bucket's capacity, with no double spending.
A cache keyed by the request, not the client
The cache key is method, path and sorted query, never the API key, so one answer serves everyone asking the same thing. A client that already holds the version gets a 304 and no body at all.
A breaker with a single probe
When an upstream falls over, the gateway answers 503 itself and instantly, instead of holding connections until the timeout. Exactly one request becomes the half-open probe, and the transition is an atomic UPDATE rather than a race.
Percentiles without the raw log
Latencies fold into a nine-bucket histogram rolled up per minute, written by the same statement as the log row. The screens read the rollup, so p95 over a day costs the same on a thousand requests and on a million.
Honest refusals
A malformed key is rejected by shape before it reaches the database, a body over the limit never leaves the gateway, and every outcome, refusals included, lands in both the log and the funnel. Clients get Retry-After and X-RateLimit-*, not silence.