Six decisions, and the failure mode nobody specifies
coreintermediateDesigning a rate limiter is six decisions taken in order, and skipping any one of them produces a limiter that works in testing and does something surprising in production. The identity key is what a request is counted against — a user id, an API key, an IP address, a tenant — and it must be something an attacker cannot cheaply change, which is why IP alone is weak and an authenticated identity is strong. The scope is what the limit protects: a whole account, one endpoint, one expensive operation, or the shared resource behind them. The algorithm decides how the count is measured over time. The storage is where counters live, which for anything beyond one process means a shared store. Distributed coordination is how several enforcement points agree, and it always involves a trade between accuracy and the cost of synchronising. And the sixth — the one that is almost never written down — is the failure mode: what the limiter does when its own storage is unavailable. It has exactly two answers, and both are wrong in different situations. Fail open means allow the request, which keeps the product working during a cache outage and removes all protection at the moment the system is least healthy. Fail closed means reject, which preserves protection and turns a limiter-store outage into a full product outage. The right answer differs per limit: a login-attempt limiter protecting against credential stuffing should fail closed, while a generous per-user API quota should fail open. A limiter without a stated failure mode has one anyway — whatever the code does when the store call throws — and it is chosen by accident.
Think of it as
A limiter is a turnstile, and the six decisions are: whose ticket you check, which door it controls, how you count entries over time, where the tally is kept, how several turnstiles keep one tally, and what happens when the tally system goes dark. Every real deployment eventually experiences the last one. Whether the turnstiles then swing free or lock shut is a policy decision that costs money either way, so it is worth making deliberately rather than discovering during the incident.
What we're doing: Run the same limiter-store outage against two limits with different failure modes.
- 6
- Failing closed on login is a deliberate six-minute sign-in outage. That is a real cost, accepted because the alternative is removing the only protection against an attack that is more likely during an incident, not less.
- 14
- Failing open here costs at most some over-quota usage by one client for six minutes. The limit exists to keep clients fair to each other, and a fairness rule is not worth an outage.
- 24
- This is what a limiter without a stated failure mode actually does. It is the worst of both policies — protection is lost for anything that would have been allowed, availability is lost for everything else, and the response code tells the caller nothing useful.
Why this works: The failure mode is a property of what each limit is for, not of the limiter implementation, which is why it cannot be set once globally. Writing it down per limit turns an incident's worst moment into a behaviour someone already reasoned about, and gives operators a defensible answer to "why could nobody log in".
Calling the counter store without a timeout
Wrong
Better
What you see: A degraded — not dead — counter store makes every rate-limited endpoint slow rather than failing them fast, so request workers pile up waiting on the limiter and the service exhausts its connection pool. The limiter, added to protect the service, is what takes it down.
Why: A limiter runs on the request path, so its own latency is added to every guarded request. Without a bound, a slow store converts directly into slow requests and then into exhausted capacity — which is why a limiter needs both an explicit timeout and a decision about what to do when that timeout fires.
- Identity key — hard for an attacker to change
- Scope — what is being protected
- Algorithm — how the count is measured
- Storage — where the counter lives
- Coordination — how enforcement points agree
- Failure mode — open or closed when the store is down
The six decisions, in the order they are made
Fail open versus fail closed, per limit
Remember: Six decisions in order: identity key (expensive for an attacker to change), scope (what is protected), algorithm, storage, distributed coordination, and failure mode. The sixth is the one that is almost never written down and always exists anyway. Choose it per limit — abuse-prevention limits fail closed, fairness quotas fail open — and always bound the limiter's own store call with a timeout, since a limiter that hangs turns a slow cache into a slow product no matter which policy you picked.
See also: atomic counters and edge enforcement · rate limit scope · rate limiting algorithms · distributed rate limiting · connect read request deadlines · defining degraded mode

