A 429 Too Many Requests response means the server is telling you exactly what happened and, usually, exactly what to do about it — unlike vaguer 4xx/5xx errors, a well-implemented 429 typically includes a Retry-After header specifying how long to wait, making this one of the more actionable HTTP errors to handle correctly.
This error means you've exceeded a rate limit the server enforces — whether that's requests per second, per minute, or a burst allowance — and the server is deliberately rejecting the request rather than processing it, as a protective measure against overload or abuse.
Why This Error Happens
Rate limiting exists to protect server resources and ensure fair usage across clients. A 429 fires when your request count within a given time window exceeds the limit the server has configured for your API key, IP address, or account tier — this is a deliberate, expected response, not a bug, and well-designed APIs signal both the limit and the reset timing so clients can respond correctly rather than guessing.
Reproducing and Diagnosing the Error
A client hammering an API without respecting rate limits:
// Naive: fires 100 requests immediately, ignoring any rate limit
const results = await Promise.all(
userIds.map((id) => fetch(`https://api.example.com/users/${id}`))
);
// Many of these will return 429 once the limit is exceeded
Checking the response headers a well-behaved API provides:
const response = await fetch("https://api.example.com/users/123");
if (response.status === 429) {
const retryAfter = response.headers.get("Retry-After"); // seconds to wait
const remaining = response.headers.get("X-RateLimit-Remaining");
console.log(`Rate limited. Retry after ${retryAfter}s. Remaining: ${remaining}`);
}
Core Concepts Behind This Error
The Retry-After header, when present, tells you exactly how long to wait before retrying, either as a number of seconds or an HTTP date — respecting this header rather than retrying immediately (or on your own arbitrary schedule) is the correct way to handle a 429 from any well-implemented API.
Rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) let you proactively throttle your own request rate before hitting the limit, rather than reactively handling 429s after they occur — checking remaining quota before firing a batch of requests is more efficient than retry-based handling alone.
Exponential backoff with jitter is the standard retry strategy when Retry-After isn't provided, increasing the wait time between retries progressively (with some randomness added to avoid many clients retrying in lockstep) rather than retrying at a fixed interval, which can itself contribute to sustained rate limit pressure.
Concurrent request batching without throttling is the most common cause of self-inflicted 429s — firing many requests simultaneously (as with an unbounded Promise.all) often exceeds rate limits that sequential or properly-throttled requests would respect comfortably.
Fixing "429 Too Many Requests"
Fix 1: Respect the Retry-After header explicitly rather than retrying immediately:
async function fetchWithRateLimitHandling(url: string): Promise<Response> {
const response = await fetch(url);
if (response.status === 429) {
const retryAfter = Number(response.headers.get("Retry-After") ?? 1);
await new Promise((r) => setTimeout(r, retryAfter * 1000));
return fetchWithRateLimitHandling(url);
}
return response;
}
Fix 2: Throttle concurrent requests explicitly rather than firing them all at once, using a concurrency limiter to stay within the server's rate limit proactively:
import pLimit from "p-limit";
const limit = pLimit(5); // max 5 concurrent requests
const results = await Promise.all(
userIds.map((id) => limit(() => fetch(`https://api.example.com/users/${id}`)))
);
Fix 3: Implement exponential backoff with jitter for retries when no Retry-After header is provided:
async function fetchWithBackoff(url: string, attempt = 0): Promise<Response> {
const response = await fetch(url);
if (response.status === 429 && attempt < 5) {
const delay = Math.min(1000 * 2 ** attempt + Math.random() * 500, 30000);
await new Promise((r) => setTimeout(r, delay));
return fetchWithBackoff(url, attempt + 1);
}
return response;
}
Should You Retry Every 429, or Sometimes Back Off Entirely?
Retry when the rate limit is clearly per-request-window and temporary — the standard case, well-suited to backoff or Retry-After-based waiting. Back off more substantially, or queue the work for significantly later, when you're hitting a daily or account-level quota rather than a short window — retrying aggressively against a quota that resets once a day just wastes attempts and can look like abusive behavior to the API provider.
Preventing 429 Errors in Production
Proactively throttle request concurrency and rate to stay comfortably under known API limits rather than relying solely on reactive retry handling, and always check for and respect Retry-After and rate-limit headers when present. Build backoff-with-jitter retry logic as a standard utility used across all your external API calls, since rate limiting is a near-universal characteristic of production APIs, not an edge case specific to one integration.
If you're hitting 429s consistently, check whether you're respecting the API's documented rate limits with proactive throttling before adding more retry logic — retries handle occasional bursts, but consistent 429s mean your baseline request rate itself needs to come down.