HTTP 429 Too Many Requests
429 means rate limiting. It isn't defined in RFC 9110 but in RFC 6585 (2012). The server may include a Retry-After header saying how long to wait, and should explain the limit in the body.
429 Too Many Requests
The client has sent too many requests in a given amount of time.
Who fixes it: Client. The client should slow down and retry later. If legitimate traffic is being limited, the server's limits need adjusting.
What 429 means
RFC 6585 doesn't say how the server identifies the client (IP, API key, user, account) or what it counts (requests, tokens, concurrent connections). The same 429 can mean "this IP is too noisy" or "your plan's monthly quota is used up".
RFC 6585 says responses with a 429 status must not be stored by a cache.
Source: RFC 6585 §4.
Common causes
- A retry storm: code that retries immediately on failure, multiplying traffic exactly when the server is struggling.
- Unbounded parallelism:
Promise.all()over 1,000 items fires 1,000 requests at once. - Many clients sharing one IP (office NAT, CI runners, serverless egress) against an IP-based limit.
- Per-minute or per-day quotas on third-party APIs such as OpenAI, Stripe or Google APIs, often reported in
RateLimitorX-RateLimit-*headers. - Stricter limits on login and password-reset endpoints to slow down brute-force attempts.
- Crawlers, scrapers or your own monitoring hitting a site faster than its limit.
How to handle and send a 429
Client: honour Retry-After, otherwise back off with jitter
Retry-After is either a number of seconds or an HTTP date. Without it, use exponential backoff with random jitter so many clients don't retry in lockstep, and cap the retries.
async function fetchWithRetry(url, options = {}, maxRetries = 5) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, options);
if (res.status !== 429 || attempt === maxRetries) return res;
const header = res.headers.get('Retry-After');
let delay;
if (header && !Number.isNaN(Number(header))) {
delay = Number(header) * 1000; // seconds
} else if (header) {
delay = Math.max(0, Date.parse(header) - Date.now()) || 1000; // HTTP date
} else {
delay = Math.random() * Math.min(30_000, 500 * 2 ** attempt); // full jitter
}
await new Promise((resolve) => setTimeout(resolve, delay));
}
}nginx: limit_req returns 503 unless you change it
limit_req_status defaults to 503, which tells clients and crawlers the server is down. Set it to 429.
http {
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
server {
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_req_status 429; # default is 503
proxy_pass http://127.0.0.1:3000;
}
}
}Express: express-rate-limit
It responds 429 with Retry-After and the standard RateLimit headers. Behind a proxy, set trust proxy or every client shares the proxy's IP.
import { rateLimit } from 'express-rate-limit';
app.set('trust proxy', 1);
app.use('/api/', rateLimit({
windowMs: 60 * 1000, // 1 minute
limit: 100, // requests per IP per window
standardHeaders: 'draft-7', // RateLimit + RateLimit-Policy headers
legacyHeaders: false,
}));Limit concurrency instead of firing everything at once
Process work in small batches or with a concurrency limiter such as p-limit, so you stay under the limit in the first place.
Headers that matter with 429
- Retry-After
Retry-After: 120(seconds) orRetry-After: Fri, 16 Oct 2026 10:00:00 GMT. Clients should wait at least that long.- RateLimit / RateLimit-Policy
- IETF draft headers with the limit, remaining quota and reset time. Many APIs still use the older
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset.
429 vs similar status codes
- 503 Service Unavailable: The server as a whole is overloaded or down. 429 is about one client's usage.
- 403 Forbidden: Access denied regardless of rate. Some APIs misuse 403 for rate limits; prefer 429.
SEO impact of 429
Google treats 429 like a server error: Googlebot slows its crawl rate, and if 429s persist, URLs already indexed can be dropped. Don't rate-limit Googlebot unless your server truly can't cope, and if you have to, 429 or 503 is the right signal, not 403 or 404. Verify Googlebot by reverse DNS, not by user agent.
Related status codes
503Service Unavailable
The server is temporarily unable to handle the request, due to overload or scheduled maintenance.
403Forbidden
The server understood the request but refuses to fulfil it.
401Unauthorized
The request lacks valid authentication credentials for the target resource.
Looking for another code? Search the full HTTP status codes list, or read which code your API should return in the REST API status codes guide.
Learn more
HTTP Status Codes for REST APIs: 200 vs 201 vs 204, 400 vs 422 vs 429
A practical guide to choosing the right HTTP status code in REST API design. Covers the most confusing pairs: 200 vs 201, 400 vs 422, 401 vs 403, and when to use 503 vs 429.
REST API Design Best Practices: A Practical Guide
The conventions that make REST APIs intuitive and consistent. Covers URL design, HTTP methods, status codes, versioning, pagination, and error responses.
CORS Explained: Why It Happens and How to Fix It
CORS errors happen when a browser blocks a request to a different origin. Learn how the Same-Origin Policy works, what preflight requests are, and how to configure CORS correctly in Express, FastAPI, and Nginx.
Frequently Asked Questions
How long should I wait after a 429?▾
If the response has a Retry-After header, wait that long; it's either a number of seconds or an HTTP date. Without it, back off exponentially (for example 0.5s, 1s, 2s, 4s) with random jitter, and cap the number of retries.
Is 429 a client or server error?▾
It's a 4xx, so the server attributes it to the client sending too much. The fix can be on either side: the client should slow down and retry later, and the server owner may need to raise limits that are too tight for legitimate traffic.
Why does nginx return 503 instead of 429 when rate limiting?▾
limit_req_status and limit_conn_status default to 503. Add limit_req_status 429; next to your limit_req directive so clients and crawlers can tell rate limiting apart from a real outage.