What a rate limit can key on

A limiter can only bucket by what the request carries when it runs, so picking a key is picking a stage: an address first, then a credential nobody checked.

Six translucent glass key slots hover in a row on black, with the fourth slot glowing orange

A rate limiter can only bucket by what the request carries when it runs, so choosing a key is choosing a stage in the pipeline. Early there is nothing but a network address. Later there is a credential the request asserts before anything verifies it. The ticket saying an entire office gets 429 comes out of that ordering.

A limiter keys on whatever the request carries at that line

A limiter is middleware, and middleware has a position. Everything about which keys are available to it follows from where it sits, because a key has to be derived from facts the request has already established at that line.

At the front door, the request has established very little. There is a body nobody has validated, headers anybody can write, and the address the packets arrived from. Of those, only the address is outside the caller’s control, and even that is a weaker fact than it looks. A credential is the better subject, but only once something has read it, and middleware runs ahead of the code that does the reading. What a limiter usually holds is a credential the caller claims, and a claim costs nothing to manufacture.

What the request has established at that lineWhat a key can beWho shares the bucket
Packets arrived from an addressThe address, normalizedEveryone behind that egress, and on IPv6 everyone in the same /56
A credential was presented, and nothing has read itA digest of the string presentedWhoever sends that exact string
A credential was read and rejectedThe address, againEveryone behind that egress

The middle row is weaker than it looks and the third row is awkward by construction. The last two sections are about how they hold each other up.

Before identity, the only subject is an address

Take a public endpoint a visitor’s browser calls, where the request names a project by a key that is published in page source. The cap on it has to run before that key is resolved against the database, because the database read performed for an unauthenticated stranger is part of what the cap is protecting. So the limiter sits ahead of every lookup, and the address is all it has.

Using the address raw does not work, in two separate ways.

The first is normalization. IPv4 passes through unchanged, an IPv4 address written in IPv6 form is rewritten back to its IPv4 spelling, and an IPv6 address is truncated to its /56 prefix. Skip that step and an IPv6 client gets a fresh bucket for every request by walking to the next address in a range it already holds, which is the whole cap gone rather than a cap slightly off.

The second is that we do not want to keep the address at all. The key is an HMAC of it under a salt, so the store holds a digest and nothing reversible. That is what forces the order: the digest destroys the structure the /56 collapse needs, and there is no way to normalize a hash afterwards. Normalize, then HMAC. Both orders produce a key that looks perfectly reasonable in a debugger, and only one of them caps anything.

The property this buys is worth naming, because it is the reason to accept a coarse key at all: a caller cannot escape the bucket by rotating the public project key, since the limiter has not read it yet.

Why the whole office gets 429

Two hundred people behind one corporate egress are one address, and therefore one bucket. Nothing about the limiter is malfunctioning when the ticket arrives. It answered a question about the only subject it had, and that subject happens to be a building.

The same shape shows up wherever addresses are shared: a VPN concentrator, carrier-grade NAT on a mobile network, a CI runner pool, a university. IPv6 does not rescue it either. A limiter that buckets by /56 is treating a whole subscriber site as one caller, which is the correct reading given that RFC 6177 expects an end site to receive a prefix of roughly that size rather than a single address.

Once that is clear, the fix stops being a search for a cleverer key. There is no better subject in the request yet. What is left is to size the shared cap for a building rather than for a person, and to put the tight cap on the credential, where a bucket belongs to one caller and a noisy neighbour cannot spend somebody else’s allowance. That is where the next section starts, and it is where the interesting problem is, because a credential is a fact the request asserts before anybody has checked it.

A bucket per token, before anything has read the token

Our MCP endpoint allows 240 calls a minute per token, which is the ceiling the developer reference publishes, and the bucket is a SHA-256 digest of the bearer token carrying a namespace prefix so a credential key can never collide with an address key in the same store.

It is worth being exact about where that runs. Both limiters on the endpoint are mounted ahead of the JSON body parser and ahead of the handler that resolves a token against the stored hashes, so the key function only ever reads a header: it takes whatever follows Bearer in Authorization, hashes it, and returns. Nothing has compared that string to anything.

So the bucket belongs to the token presented, never to a token somebody accepted, and by itself that caps very little. A caller sending a fresh random bearer on every request opens a fresh 240-a-minute bucket every time, because a different string is a different key. A key the caller chooses is not a ceiling.

What turns it into one is the limiter standing in front of it. The failure limiter is mounted first, it keys on the address, and it charges every request that does not come back under 400. An invented token opens its own generous bucket on the per-token limiter and collects a 401 from the handler behind it, and that 401 is billed back to the address. Manufacturing credentials buys allowance under a key the caller controls and spends allowance under the one key they do not, which is why the pair caps what neither half caps alone.

The digest is a storage decision, not a security control. Rate limit keys live for the length of the window, get logged, and turn up in metrics labels, so keying on the raw token would create a second copy of every credential in places nobody threat models. Bucketing behaves identically either way.

The ceiling is high on purpose. An agent working through a real triage session is bursty and legitimate, and the limit exists to cap what a leaked token can drain rather than to pace ordinary use. What the per-token key buys, read together with the limiter in front of it, is isolation between real holders: two valid tokens are two buckets, so one client having a bad day cannot take anybody else’s allowance, which is exactly the property the address stage cannot offer.

One line in that key function decides the rest. If there is no bearer token at all, the key falls back to the address. An unauthenticated caller is therefore back at stage one, sharing the building’s bucket with everybody else who has not identified themselves. The published per-token ceiling starts applying the moment a token is presented, which is a different moment from the one it is honoured, and that gap is the same lesson the ladder of reporter identity provenance teaches from the other end of the pipeline: what a value proves is a separate question from what it is worth.

The limiter that cannot wait for identity

Now the third row of that first table. A limiter whose job is to bound credential guessing has a problem no key choice solves: it fires precisely when the credential turned out to be wrong. Keying on the credential would hand a guesser a brand new bucket with every wrong guess, which counts attempts and stops none of them. So it keys on the address, sits at stage one, and inherits the office problem by construction.

The way out is a refund rather than a better key. Everything that does not come back under 400 is counted, which takes in 5xx as well as 4xx, and everything below that line is refunded once the response has gone out. A caller who authenticates successfully is therefore never charged against the shared bucket at all. Failures accumulate; working traffic does not.

Collateral damage falls sharply and does not vanish. Sixty failed requests a minute is the entire budget, and it belongs to your egress rather than to you: one client in your office looping on an expired token can spend all of it, and the next colleague through the door is refused while holding a perfectly good credential. The limiter has to decide before it knows whether that credential was any good, which is the same constraint from the top of this post arriving at its most expensive form. On our side the address does not survive that decision as an address: our security practices keep it as a one-way salted hash, if at all.

So the question worth asking about a limiter is how far down the pipeline it sits, because that position has already decided which of its keys are honest ones. A key can always name something the request has not proved yet; what it cannot do is make the naming true. Either move the limiter to where the proof is, or put a second one in front of it that charges for the claim.

Common questions

Why is our whole office getting 429 from one API?

A limiter running before authentication has only the network address to bucket by, and every person behind one office egress presents the same address. The bucket belongs to the building, so one badly behaved client spends the allowance for everybody sharing that connection.

Should a rate limiter key on the IP address or on the API key?

Both, and the pairing matters more than the choice. A limiter placed ahead of authentication can key on the credential a caller presents, but the caller picks that string, so it only caps anything while a second limiter keyed on the address counts the failed attempts in front of it. A digest of a verified credential is the strong key, and it is available only after the check.

Why do rate limiters collapse IPv6 addresses to a /56 subnet?

Because a single IPv6 customer is normally assigned a whole prefix rather than one address. Keying on the full address would let one client take a fresh bucket per request simply by changing the low bits, which removes the cap entirely rather than enforcing it.

Is it safe to use an API token as a rate limit key?

Use a digest of it. Rate limit keys are stored for the length of the window and often end up in logs and metrics, so keying on the raw token creates a second copy of every credential in a place nobody threat models. A namespaced hash buckets identically.

How do you throttle failed authentication without punishing valid callers?

Count only the requests that failed. A limiter guarding credential guessing has to key on the address, since the credential is exactly the thing that turned out to be wrong, so refunding every successful request is what keeps a legitimate caller off the shared bucket.

What rate limit applies to the Notarus MCP endpoint?

240 calls a minute per bearer token, answered with HTTP 429 and a JSON-RPC error when exceeded. The ceiling is deliberately high, because it exists to cap what a leaked token can drain rather than to pace an agent doing real work, and it is paired with a second limiter that caps failed requests per network address.

The Notarus team

The people who build Notarus. We write up the decisions behind the product when the reasoning is worth more than the changelog line.

Keep reading

Reports with the evidence attached.

Start free, one script tag.

<script src="…/widget.js" data-key="pb_pub_…">