Every 429 Too Many Requests should carry a second answer: when the client may try again. Send Retry-After with the 429, send the limit, the remaining quota and the reset time on every response, and write clients that wait at least as long as they are told, then back off with random jitter when they are told nothing.
The short answer
- If you run the API: answer 429, add
Retry-Afterin seconds, and send limit, remaining and reset headers on every response so clients can slow down before they hit the wall. - If you call the API: when
Retry-Afteris there, wait at least that long. When remaining is 0, wait for the reset. Otherwise back off exponentially with full jitter, and stop after a fixed number of tries. - Never retry in a tight loop. GitHub warns that "Continuing to make requests while you are rate limited may result in the banning of your integration" (GitHub REST API docs).
What does a 429 status code mean?
429 Too Many Requests is the HTTP status for rate limiting. RFC 6585, published in April 2012, defines it in one sentence: "The 429 status code indicates that the user has sent too many requests in a given amount of time ("rate limiting")." The same section says the response "MAY include a Retry-After header indicating how long to wait before making a new request", and that "Responses with the 429 status code MUST NOT be stored by a cache" (RFC 6585, section 4).
The RFC leaves the rules to you: it "does not define how the origin server identifies the user, nor how it counts requests." Per API key, per IP, per token used: that is your call, and your documentation's job to explain.
HTTP/1.1 429 Too Many Requests 1 Content-Type: application/json Retry-After: 30 2 RateLimit-Policy: "default";q=100;w=60 3 RateLimit: "default";r=0;t=30 4 {"error": "rate_limited", "message": "100 requests per minute per API key. Try again in 30 seconds."} 5
- Status line. 429, not 500 and not a generic 403: the client can tell "slow down" from "broken" or "forbidden".
- Retry-After. Seconds to wait (or an HTTP date). The standard header for this (RFC 9110, 10.2.3).
- RateLimit-Policy. The quota: 100 units (
q) per 60-second window (w). Draft syntax, see below. - RateLimit. What is left: 0 units (
r) for the next 30 seconds (t); the draft says not to assume the full quota is back after that. Draft syntax. - Body. RFC 6585 says the response "SHOULD include details explaining the condition". Say which limit was hit and when it lifts.
What do Retry-After, RateLimit and X-RateLimit headers mean?
Five ideas cover every rate limit header you will meet. The names change from API to API; the ideas do not.
- Retry-After
- How long to wait before the next request. RFC 9110: "The Retry-After field value can be either an HTTP-date or a number of seconds to delay after receiving the response." Its own examples are
Retry-After: Fri, 31 Dec 1999 23:59:59 GMTandRetry-After: 120, "the delay is 2 minutes" (RFC 9110, 10.2.3). A 503 Service Unavailable may carry it too (RFC 9110, 15.6.4). - Limit
- The quota for one window, such as 5,000 requests per hour. Usually
x-ratelimit-limitor a variant of it. - Remaining
- What is left of the quota in the current window. When it reaches 0, the next request is refused.
- Reset
- When the quota comes back. The one value with no common format: an epoch time, a duration or a timestamp, depending on the API (table below).
- RateLimit-Policy and RateLimit
- Two fields proposed by the IETF HTTPAPI working group to replace the many
X-spellings.RateLimit-Policystates the quota (q), the window in seconds (w, optional), an optional unit (qu: requests, content-bytes or concurrent-requests) and an optional partition key (pk).RateLimitstates the quota left (r) and the effective window in seconds within which no more thanrmay be used (t, optional). The latest version is draft -11 of 23 May 2026; it is still an Internet-Draft, not an RFC, and it expires on 24 November 2026 unless replaced (IETF datatracker).
How do GitHub, OpenAI, Anthropic and Discord write these headers?
Four popular APIs, four ways to say when the quota comes back. A client that talks to more than one of them needs a reset parser per API, or it can use Retry-After when it is sent. All four document it, but not every 429 carries it: GitHub sends it for secondary limits, and Anthropic's spend-cap 429 never has it.
| API | Quota headers | Reset value | On a 429 |
|---|---|---|---|
| GitHub REST | x-ratelimit-limit, -remaining, -used, -resource |
x-ratelimit-reset: UTC epoch seconds |
403 or 429. Honour retry-after if present, else wait for the reset if remaining is 0, else wait at least one minute. |
| OpenAI | x-ratelimit-limit-requests, -tokens, x-ratelimit-remaining-requests, -tokens |
x-ratelimit-reset-requests and -tokens: a duration, such as 1s or 6m0s |
429; Retry-After "when present", to be treated "as a minimum". |
| Anthropic | anthropic-ratelimit-requests-limit, -remaining, plus tokens, input-tokens and output-tokens variants |
anthropic-ratelimit-requests-reset: an RFC 3339 timestamp |
429 rate_limit_error; retry-after in seconds, but "Not every 429 carries it; the spend-cap 429 never does". |
| Discord | X-RateLimit-Limit, -Remaining, -Bucket |
X-RateLimit-Reset: epoch seconds with decimals (1470173023.123), and X-RateLimit-Reset-After: seconds left (64.57) |
429 with a JSON body; use Retry-After or the body's retry_after. |
All four docs read on 2026-10-21. GitHub adds that its rate limit values "can vary from one response to the next based on the location handling the request", so "avoid logic that depends on an exact remaining count." Anthropic notes it uses "the token bucket algorithm", so capacity "is continuously replenished up to your maximum limit, rather than being reset at fixed intervals."
Should my client retry this request?
Ask these four questions before every retry, in order. The first answer that says wait, retry or stop decides.
-
Is the status 429 or 503?
No Do not retry on the rate limit path. One exception: GitHub can answer a rate limit with 403, so check its headers first.
Yes Go on.
-
Have you hit your retry limit, or is this a limit that does not lift by itself?
Stop Show the error. GitHub says to "throw an error after a specific number of retries"; Anthropic's spend-cap 429 has no
retry-afterand retrying "fails until access resumes".No Go on.
-
Is there a
Retry-Afterheader?Yes Wait at least that long. OpenAI also asks you to "add a small random delay so multiple clients don't retry at the same time" (OpenAI docs). Then retry.
No Go on.
-
Is remaining 0, with a reset time you can read?
Yes Wait until the reset, parsed in that API's own format. Then retry.
No Back off with full jitter (next question). For a GitHub secondary limit, wait at least one minute first.
For requests that change data, such as a POST, retry only when the API says a 429 means the request was not processed, or when it supports idempotency keys. Otherwise a retry can run the same action twice.
How long should a client wait between retries?
When the server gives no wait time, use exponential backoff with full jitter: double the ceiling after every failed try, cap it, and wait a random time between zero and that ceiling. Marc Brooker's AWS Architecture Blog post (4 March 2015) writes it as sleep = random_between(0, min(cap, base * 2 ** attempt)). In his simulation "The no-jitter exponential backoff approach is the clear loser", and he concludes that jittered backoff "should be considered a standard approach for remote clients" (Exponential Backoff And Jitter).
// fetch with retries: Retry-After first, full jitter otherwise
// use it for reads, or for writes the API makes safe to repeat (idempotency keys)
async function fetchWithRetry(url: string, init: RequestInit = {}, maxRetries = 5) {
const base = 1000
const cap = 32_000
const maxWait = 120_000 // never sleep longer than 2 minutes inside one call
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, init)
if (res.status !== 429 && res.status !== 503) return res
if (attempt === maxRetries) return res // give up, let the caller show the error
let wait = Math.random() * Math.min(cap, base * 2 ** attempt) // full jitter
const header = res.headers.get('retry-after')
if (header) {
const seconds = Number(header)
const ms = Number.isNaN(seconds) ? Date.parse(header) - Date.now() : seconds * 1000
if (ms > maxWait) return res // too long to wait here, let the caller decide
if (ms >= 0) wait = ms + Math.random() * 1000 // at least Retry-After, plus a little jitter
}
await res.body?.cancel() // free the connection before sleeping
await new Promise((resolve) => setTimeout(resolve, wait))
}
}
A Retry-After that is not a number and not a valid date gives NaN, so the loop falls back to the jittered wait, as OpenAI recommends when the header is "missing or invalid". maxRetries caps the number of tries and maxWait caps a single wait; OpenAI's advice is to "Limit both the number of attempts and the total time spent retrying".
What should each side do?
If you run the API
- Answer rate limits with 429 and a body that names the limit that was hit.
- Add
Retry-Afterin seconds. Every HTTP client can read a number. - Send limit, remaining and reset on every response, not only on the 429, so clients can slow down early.
- Document what you count (requests, tokens, per key or per IP), the window, and the format of the reset value.
- Make sure no cache or CDN stores a 429: RFC 6585 says it "MUST NOT be stored by a cache".
- If you adopt
RateLimit-PolicyandRateLimit, keep your current headers too: the draft can still change.
If you call the API
- Treat
Retry-Afteras the minimum wait, then add a little random delay. - Parse each API's reset format: epoch, duration or timestamp.
- Back off with full jitter and a cap when the server gives no wait time.
- Stop after a fixed number of retries and show a clear error.
- Watch remaining and slow down before it reaches 0, without trusting the exact count.
- Log the rate limit headers of every 429. They tell you which limit you hit.
Quick questions
Should a rate limit return 429 or 403?
429. RFC 6585 created it for exactly this case. Some APIs still use 403 for some limits (GitHub returns "a 403 or 429 response"), so a client should read the headers, not only the status.
Should Retry-After be seconds or a date?
Both are valid (RFC 9110). Seconds are simpler: the client does not need to parse a date or trust that its clock matches yours.
Are the RateLimit and RateLimit-Policy headers a standard yet?
No. They are in draft -11 (23 May 2026), which says it is "inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress"" (IETF).
Which rate limiting algorithm should I use?
A token bucket is the common choice: Stripe uses it ("We use the token bucket algorithm to do rate limiting", Stripe, 2017), and so does Anthropic. Cloudflare described an approximate sliding window in 2017; in its test on 400 million requests, "0.003% of requests have been wrongly allowed or rate limited" (Cloudflare, 2017).
Is one rate limit enough?
Not always. Stripe runs "4 different types of limiters in production": a request rate limiter, a concurrent requests limiter and two load shedders that protect the fleet when it is under strain (Stripe).
Sources
- RFC 6585, section 4, 429 Too Many Requests (April 2012).
- RFC 9110, section 10.2.3, Retry-After and section 15.6.4, 503 Service Unavailable (June 2022).
- IETF, RateLimit header fields for HTTP, draft-ietf-httpapi-ratelimit-headers-11 (23 May 2026).
- GitHub Docs, Rate limits for the REST API.
- OpenAI API docs, Rate limits.
- Claude API docs, Rate limits.
- Discord Developer docs, Rate limits.
- Marc Brooker, Exponential Backoff And Jitter, AWS Architecture Blog (4 March 2015).
- Paul Tarjan, Scaling your API with rate limiters, Stripe (30 March 2017).
- Julien Desgats, How we built rate limiting capable of scaling to millions of domains, Cloudflare (7 June 2017).
- All pages read on 2026-10-21.



