429 Too Many Requests
Client error response defined by RFC 6585 §4.
Last updated July 27, 2026
Common causes at a glance
- A client exceeding a documented per-token or per-endpoint quota
- A per-IP edge limit tripped by many users behind one NAT or proxy
- A retry storm where failures are retried faster than the limit allows
- A burst or concurrency limit fired even though the average rate is fine
Reproduce it
curl -sS -o /dev/null -D - https://api.example.com/v1/search | grep -i -E 'HTTP/|retry-after|ratelimit'What 429 Too Many Requests means
429 says nothing about what you asked for and everything about how often you asked. The request was well-formed and probably authorised; a counter somewhere hit its ceiling. The critical detail is which counter.
Edge limits are per-IP and enforced by a CDN or WAF before the application sees anything. Application limits are per-token or per-endpoint and enforced by your own code. An office behind one NAT address can trip an edge limit while every user sits under their per-token quota, and one greedy service account can exhaust a per-token quota from a dozen IPs. The response headers usually reveal which fired, if anyone set them.
What the spec says
RFC 6585 §4 defines 429 and says the representation should
explain the condition and may include a Retry-After header, defined in
RFC 9110 §10.2.3 as either delta-seconds or an HTTP-date. Two
omissions matter: the spec does not define how limits are counted, and it standardises no
header for remaining quota. The ubiquitous X-RateLimit-Limit,
X-RateLimit-Remaining and X-RateLimit-Reset trio is convention,
not specification, and vendors differ on whether Reset is a Unix timestamp or a count of
seconds. RFC 6585 also notes 429 responses may be cached, so send explicit
Cache-Control if that is not what you want.
What actually causes it
The most damaging pattern is self-inflicted: a client hits the limit, retries immediately, and the retries keep the counter pinned. Add a few dozen instances doing this in lockstep after a shared upstream blip and you have a herd that cannot recover on its own. The fix is exponential backoff with jitter — without randomisation every client wakes at the same moment and the stampede repeats on a slower cycle. Also common: a batch job that parallelises and trips a concurrency limit while its average rate looks harmless, and a shared API token used by several services so no team’s traffic explains the total.
How to debug it
Establish which limiter fired, then whether the server told you when to come back:
curl -sS -o /dev/null -D - https://api.example.com/v1/search \
| grep -i -E '^(HTTP/|retry-after|x-ratelimit|ratelimit|cf-ray|server):'
If Retry-After is present, honour it exactly; it is the only value the server
has committed to. If only X-RateLimit-* headers appear, read
Reset carefully — some APIs give a Unix timestamp and some give seconds
remaining, and treating one as the other produces a hot loop or an hour-long stall. A CDN
trace header on a 429 with no rate-limit headers points at an edge limit, which means the
quota in the API docs is not the one you tripped.
Working examples
curl
# Show status plus the headers that tell you when to retry.
curl -sS -o /dev/null -D - https://api.example.com/v1/search \
| grep -i -E '^(HTTP/|retry-after|x-ratelimit|ratelimit):'
# --retry-all-errors covers 429, and curl honours Retry-After when present.
curl -sS --retry 5 --retry-all-errors --retry-max-time 120 \
https://api.example.com/v1/search
Python (requests)
import random, time
import requests
def get_with_backoff(url, attempts=5):
for attempt in range(attempts):
resp = requests.get(url, timeout=10)
if resp.status_code != 429:
return resp
# Retry-After wins; otherwise exponential backoff WITH jitter.
wait = resp.headers.get("Retry-After", "")
delay = float(wait) if wait.isdigit() else 2 ** attempt
time.sleep(min(delay, 60) * (0.5 + random.random()))
raise RuntimeError("still rate limited after retries")
print(get_with_backoff("https://api.example.com/v1/search").status_code)
Node (fetch)
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function getWithBackoff(url, attempts = 5) {
for (let i = 0; i < attempts; i++) {
const res = await fetch(url);
if (res.status !== 429) return res;
const header = res.headers.get("retry-after");
const base = header && /^\d+$/.test(header) ? Number(header) : 2 ** i;
// Jitter is not optional: without it every client retries in lockstep.
await sleep(Math.min(base, 60) * 1000 * (0.5 + Math.random()));
}
throw new Error("still rate limited");
}
console.log((await getWithBackoff("https://api.example.com/v1/search")).status);Reference and tooling
The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.
Other 4xx codes
- 400 Bad Request
- 401 Unauthorized
- 402 Payment Required
- 403 Forbidden
- 404 Not Found
- 405 Method Not Allowed
- 406 Not Acceptable
- 407 Proxy Authentication Required
- 408 Request Timeout
- 409 Conflict
- 410 Gone
- 411 Length Required
- 412 Precondition Failed
- 413 Content Too Large
- 414 URI Too Long
- 415 Unsupported Media Type
- 416 Range Not Satisfiable
- 417 Expectation Failed
- 418 (Unused)
- 419 Page Expired
- 421 Misdirected Request
- 422 Unprocessable Content
- 423 Locked
- 424 Failed Dependency
- 425 Too Early
- 426 Upgrade Required
- 428 Precondition Required
- 431 Request Header Fields Too Large
- 444 No Response
- 451 Unavailable For Legal Reasons
- 494 Request Header Too Large
- 495 SSL Certificate Error
- 496 SSL Certificate Required
- 497 HTTP Request Sent to HTTPS Port
- 499 Client Closed Request