How to Handle 429 Too Many Requests
An HTTP 429 Too Many Requests response means the server is rejecting your request because you have sent too many in a given window. It is not a permanent failure and it is rarely a bug in your code logic. It is the server asking you to slow down. The correct client-side response is to wait the right amount of time and retry, not to hammer the endpoint until it gives up. This guide covers how to read the server's instructions, when to back off on your own, and a single reusable fetch wrapper you can drop into any project.
This is about the calling side. If you are designing the limit itself, read our companion post on rate-limit strategies for APIs instead. Here, the goal is to be a well-behaved client.
The decision tree: is there a Retry-After header?
Every 429 handler reduces to one question: did the server tell you how long to wait? Per RFC 9110, a server may include a Retry-After header on a 429 (and on 503) response. If it is present, honor it exactly. You have no business guessing when the server has given you an authoritative answer.
- Retry-After is present: wait the specified duration, then retry once. Do not add your own backoff on top of it; that just delays your recovery.
- Retry-After is absent: fall back to exponential backoff with jitter, capping the number of attempts.
Retry-After has two forms, and that is where bugs live
The single most common parsing bug is assuming Retry-After is always a number of seconds. It is not. The spec defines two valid forms, and a server may use either:
- Delay-seconds form: an integer, like
Retry-After: 120, meaning wait 120 seconds. - HTTP-date form: an absolute timestamp, like
Retry-After: Wed, 21 Oct 2026 07:28:00 GMT, meaning wait until that moment.
If you call parseInt on the HTTP-date form you get NaN, and naive code then either retries instantly (turning one 429 into a storm) or crashes. Detect which form you got: if the value parses cleanly as an integer, treat it as seconds; otherwise parse it as a date and compute the delay from now. Always clamp the result to be non-negative, because a date in the past should mean "retry immediately," not a negative timeout. To double-check the exact format a server uses, paste the headers you received into our HTTP header inspector.
No header? Exponential backoff with jitter
When the server gives no Retry-After, you back off on a growing schedule: roughly 1s, 2s, 4s, 8s, doubling each attempt. But a fixed schedule has a dangerous failure mode. If many clients hit the limit at the same moment, they will all retry at exactly 1s, then 2s, then 4s, creating synchronized waves that re-trigger the limit. This is the classic thundering-herd or retry-storm problem.
The fix is jitter: add randomness so retries spread out across the window instead of stacking on the same tick. The AWS Architecture Blog covers the math; in practice, "full jitter" works well, where each delay is a random value between zero and the current backoff ceiling. Also cap the total attempts (three to five is typical) so a genuinely down service does not trap your request in an infinite loop.
Throttle proactively with X-RateLimit headers
Reacting to 429s is recovery. Avoiding them is better. Many APIs (GitHub, Stripe and others) expose your current quota on every response through informal X-RateLimit-* headers. These are a widely used convention rather than a formal standard, so treat them as advisory and tolerate their absence:
X-RateLimit-Limit— the ceiling for the current window.X-RateLimit-Remaining— requests left before you get a 429.X-RateLimit-Reset— when the window resets (often a Unix timestamp; check the specific API's docs).
If X-RateLimit-Remaining hits zero or near-zero, pause your own outgoing requests until the reset time rather than firing the next call and eating a guaranteed 429. There is also an emerging standardized field, RateLimit, from the IETF, but the X- prefixed versions remain the most common in the wild. Use the right status semantics throughout; if you are unsure what a code means, our HTTP status codes reference explains each one.
A copy-paste fetchWithRetry
This helper ties it together: it honors Retry-After in both forms, falls back to exponential backoff with full jitter, caps retries, and proactively waits when X-RateLimit-Remaining is exhausted.
async function fetchWithRetry(url, options = {}, maxRetries = 4) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, options);
// Proactive throttle: out of quota, wait for the reset.
const remaining = res.headers.get('X-RateLimit-Remaining');
const reset = res.headers.get('X-RateLimit-Reset');
if (res.ok && remaining === '0' && reset) {
const waitMs = Math.max(0, Number(reset) * 1000 - Date.now());
if (waitMs > 0) await sleep(waitMs);
}
if (res.status !== 429) return res; // success or non-retryable error
if (attempt === maxRetries) return res; // give up, let caller handle 429
const retryAfter = res.headers.get('Retry-After');
const delayMs = retryAfter
? parseRetryAfter(retryAfter) // honor server instruction
: backoffWithJitter(attempt); // fall back to backoff
await sleep(delayMs);
}
}
function parseRetryAfter(value) {
const seconds = Number(value);
if (Number.isInteger(seconds)) return seconds * 1000; // delay-seconds form
const date = Date.parse(value); // HTTP-date form
return Number.isNaN(date) ? 0 : Math.max(0, date - Date.now());
}
function backoffWithJitter(attempt) {
const ceiling = Math.min(1000 * 2 ** attempt, 30000); // 1s,2s,4s... cap 30s
return Math.random() * ceiling; // full jitter
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
Gotchas worth remembering
- Do not retry non-idempotent requests blindly. Retrying a
GETis safe. Retrying aPOSTthat already succeeded server-side but failed to return can create duplicates. Use an idempotency key when the API supports one. - Cap total wait time, not just attempt count. A 429 with
Retry-After: 3600means a one-hour wait; surface that to the user or queue the work rather than blocking a request thread for an hour. - Respect the server even when it is silent. No
Retry-Afteris not permission to retry instantly. Backoff still applies. - 429 is not 503. Both can carry
Retry-After, but 429 is specifically "you, the client, are over your quota," while 503 is "the server is unavailable for everyone." The retry mechanics are the same, but the cause differs.
Handled correctly, a 429 is a routine, recoverable event. Read the header if it is there, back off with jitter if it is not, watch your remaining quota, and your client stays a good citizen instead of a denial-of-service amplifier. For broader patterns beyond rate limiting, see our guide on how to handle API errors.
Frequently Asked Questions
A 429 status means you have sent more requests than the server allows in a given time window, so it is rejecting the current one. It is a temporary, recoverable response, not a bug in your code. The correct reaction is to wait, ideally for the duration in the Retry-After header, and then retry rather than continuing to send requests.
No. Retrying immediately often triggers another 429 and can create a retry storm. If the response includes a Retry-After header, wait exactly that long. If it does not, use exponential backoff with jitter, doubling the delay each attempt and adding randomness so concurrent clients do not all retry on the same tick.
Per RFC 9110, Retry-After can be a number of seconds, like 120, or an absolute HTTP-date, like Wed, 21 Oct 2026 07:28:00 GMT. A common bug is assuming it is always seconds; calling parseInt on the date form yields NaN. Detect which form you received and clamp any computed delay to a non-negative value.
Watch the X-RateLimit-Remaining header that many APIs return on every response. When it reaches zero or near-zero, pause your outgoing requests until the time in X-RateLimit-Reset instead of firing another call that is guaranteed to fail. These headers are a widely used convention rather than a formal standard, so tolerate their absence.
Retrying idempotent requests like GET is safe. Retrying a non-idempotent request such as a POST that may have already succeeded server-side before failing to respond can create duplicate records or charges. For those, use an idempotency key if the API supports one so the server can deduplicate, or surface the error to the user.