Chapter 07 of 10
Rate Limits as a Design Decision
What a published limit promises a caller, why 429 needs Retry-After, how little of the header convention is standardised, and what a limit should be counted against.
A rate limit is usually introduced as an operational control, and it lands on the caller as part of the interface. The moment a client has to handle a 429, the limit is in your contract whether or not you wrote it down.
What a limit tells a caller
A published limit is more useful to a well behaved caller than an unpublished one is to you. It tells them how to size a batch job, when to add a queue and whether your API can support the integration they are planning.
An unpublished limit produces the opposite behaviour. A caller who does not know the ceiling finds it by hitting it, usually in production, and the way they discover it is a failure they did not expect. They then guess a safe rate, which is either too slow for them or still too fast for you.
So the design question is not whether to have a limit. It is what the limit is counted against, and how the caller learns they are approaching it.
The response when a caller exceeds it
The status code is 429 Too Many Requests, defined in RFC 6585 rather than in the core HTTP semantics. Using 503 instead sends the caller to your status page when the problem is their request rate.
The important half is what the response says next. Retry-After is defined in RFC 9110, accepts either a number of seconds or a date, and turns the caller's guess into an instruction. Without it a client backs off by whatever its library decided, which is either too eager or needlessly slow.
The body should follow the same error format as everything else, with a stable code the caller can branch on. A 429 is one of the few errors where the correct client behaviour is to wait and repeat, so it is worth being unambiguous that this one is retryable.
The headers are mostly convention
Callers would rather not hit the limit at all, which is what the quota headers are for. They report the ceiling, what is left and when the window resets, so a client can slow down before it is refused.
Almost none of that is standardised. The names beginning with X-RateLimit spread by imitation rather than by specification, and no published RFC defines them. The IETF HTTPAPI working group has an active draft for RateLimit header fields without the prefix, and its eleventh revision from May 2026 had still not been published as an RFC.
The practical consequence is that the header names are your decision and your documentation problem. Pick one set, use it on every endpoint that is limited, and declare it in the contract, because a caller cannot discover a response header from a specification that does not mention it.
What the limit counts
Three decisions sit under the number, and each one changes what the limit protects.
The partition decides who shares a quota. Per credential is the usual answer, since that is the unit a caller can reason about. Partitioning by IP address looks similar and behaves differently, because an entire office behind one address counts as one caller, and a single script there can exhaust the quota for everybody.
The unit decides what a request costs. Counting requests is simple and treats a cheap read the same as an expensive aggregation, so APIs with a wide cost range often weight requests instead. Weighting is fairer and harder to explain, which is why it needs the cost to be visible in the response.
The shape decides whether bursts are allowed. A fixed window is easy to describe and lets a caller spend the whole quota in the first second. A token bucket smooths that out and is harder to state as a single number. Whichever you pick, the caller needs to know, because it determines whether their retry after a 429 will succeed.
In Routebase
Rate limiting is enforced by your infrastructure, and what belongs in the contract is the response a limited caller gets. Routebase is where that half is declared and kept consistent.
Header policies apply a set of header components to every response whose status code matches the policy scope. A scope can be a whole class such as 4xx, or an exact code such as 429. So a single policy puts Retry-After and your quota headers on every rate limited response in a specification without anyone adding them endpoint by endpoint. The prebuilt header components cover the common cases including Retry-After, and policies cascade from organisation to project to specification with the most specific level winning.
Before a policy goes live, an impact analysis card names how many endpoints and responses it would touch and warns where it overlaps another policy. Individual responses can opt out through an exclusion with a reason rather than by narrowing the policy, and an excluded header can be restored later.
One style guide rule covers the contract side, flagging success responses that carry no rate limit headers, and it ships at informational severity so it prompts rather than blocks. Severities are configurable per organisation and per project. The Header Policies guide covers policies, scopes and exclusions, and the Style Guide covers the rule.
Frequently asked questions
What HTTP status code should a rate limited request return?
429 Too Many Requests, which is defined in RFC 6585 rather than in the core HTTP semantics. The response should say when the caller may try again, and RFC 9110 defines Retry-After for exactly that. Returning 503 instead tells the caller your service is unavailable, which sends them to your status page rather than to their own request rate.
Are X-RateLimit headers a standard?
No. The names beginning with X-RateLimit are a convention that spread by imitation, and no published RFC defines them. The IETF HTTPAPI working group has an active draft for RateLimit header fields without the prefix, and as of its eleventh revision in May 2026 it had not been published as an RFC.
Should rate limits be per user or per API key?
Per credential is the usual answer, because that is the unit a caller can reason about and the unit you can enforce. Partitioning by IP address looks equivalent and is not, since an entire office behind one address counts as a single caller and one script can exhaust the quota for everybody. Whatever the partition is, the caller has to be told what it is.
How should rate limits be documented?
In the contract rather than in a page beside it, because a limit is something client code has to handle. That means declaring the 429 response on the operations that can return it, declaring the headers that carry the quota, and stating what the limit is counted against. A limit a caller discovers in production is a limit you did not design.
Last reviewed by The Routebase Team.