< HTTP/1.1 429 Too Many RequestsHTTP 429 Too Many Requests: Causes, Retry-After and Fixes
Fix HTTP 429 Too Many Requests: read Retry-After and RateLimit headers, back off with jitter, and set up express-rate-limit v7, nginx limit_req and Cloudflare.
- Cacheable
- Only with explicit freshness
- Retry?
- Yes: honor Retry-After
- Usually sent by
- API gateway, CDN or origin app
- Spec
- RFC 6585 §4
- Often confused with
- 503
On this page
TL;DR: 429 Too Many Requests means you hit a rate limit. Pause the request loop, honor
Retry-Afterin seconds or as an HTTP date, and reduce concurrency; as the site owner, identify the limiter before changing its quota.
What you see in your client
These are the client formats for a response carrying 429 and the reason phrase Too Many Requests; the URLs are placeholders. They are derived from library source, rather than captured server output. A server-supplied reason phrase can change the Python, .NET and Spring text.
- Axios:
AxiosError: Request failed with status code 429. With the default status validation, the promise rejects; inspecterror.response.status,error.response.dataanderror.response.headers. Axios constructs this message insettle. - fetch: the promise resolves when the HTTP response arrives.
response.status === 429andresponse.ok === false; check those before reading a success payload. A network failure rejects separately. MDN documents this distinction. - Python requests:
requests.exceptions.HTTPError: 429 Client Error: Too Many Requests for url: https://api.example.com/resource. This comes fromresponse.raise_for_status(), not fromrequests.get()alone. Save the body before raising if it contains useful diagnostics. Requests source. - curl
-f:curl: (22) The requested URL returned error: 429.curl -ishows headers and the error body without fail mode;--fail-with-bodykeeps the body while returning a failing exit code. curl source, option documentation. - .NET:
System.Net.Http.HttpRequestException: Response status code does not indicate success: 429 (Too Many Requests).This is the English message fromEnsureSuccessStatusCode(); inspect theHttpResponseMessagefirst when you need its body. Runtime message template. - Spring:
WebClientResponseException.TooManyRequestswith message429 Too Many Requests from GET https://api.example.com/resourceforWebClient.retrieve(). RestTemplate’s default error handler usesHttpClientErrorException.TooManyRequests; its message can include the response body. These class names follow the Spring 6.2 WebClient source and HTTP client source.
What it means
RFC 6585 defines 429 as “the user has sent too many requests in a given amount of time”. The server decides what “user” and “time” mean, usually an IP address, API key or token over a sliding or fixed window. Read the body to identify the limit you hit: waiting addresses a time window, while a separate quota may require an account or plan change.
The response below is illustrative and uses the older draft-6 field layout, which express-rate-limit can emit. Current IETF work uses RateLimit and RateLimit-Policy; provider headers need their own interpretation.
HTTP/1.1 429 Too Many Requests
Retry-After: 30
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 30
Content-Type: application/json
{"error":"rate_limited","message":"Too many requests, retry in 30 seconds"}
Who sent it?
| Signal | Layer |
|---|---|
Server: cloudflare, CF-Ray, HTML page “Error 1015” | Cloudflare rate limiting rule |
Gateway response type THROTTLED or QUOTA_EXCEEDED, default status 429 | AWS API Gateway |
Server: nginx and log line limiting requests, excess: ... by zone "api" | nginx limit_req (status 429 only if configured) |
x-ratelimit-remaining: 0, JSON error naming a quota or “secondary rate limit” | The API provider (GitHub, OpenAI, Stripe and similar) |
RateLimit-* headers from express-rate-limit | Your own app |
Common causes by stack
- Express: express-rate-limit reaches
limitwithinwindowMsand returns 429 by default. Check the key, proxy configuration and shared store; multiple processes need a common counter if the quota is global. Configuration, proxy troubleshooting. - Django/DRF: a configured throttle rejects the request and raises
Throttled(429). InspectDEFAULT_THROTTLE_CLASSES,DEFAULT_THROTTLE_RATESand the view’s throttle scope; the built-in throttles use Django’s cache backend. DRF throttling. - Laravel: route
throttlemiddleware exceeds its named limiter and returns 429. CheckRateLimiter::for(...), theLimit::perMinute(...)value and theby(...)key; change the key if unrelated users share one bucket. Laravel routing limits. - ASP.NET Core: rate limiting middleware returns 429 when configured with
RejectionStatusCode = StatusCodes.Status429TooManyRequests, or anOnRejectedcallback that sets that status. Inspect the endpoint’s policy and partition key; a rejection can occur before the handler runs. Microsoft rate limiting. - nginx:
limit_reqrejects beyondburst; it becomes 429 withlimit_req_status 429. Look forlimiting requestsand the zone name in the error log. nginx directives. - Cloudflare: a rate limiting rule blocks the configured characteristic for its mitigation period. Find the request in Rate Limiting Analytics and inspect that rule’s expression and action. Cloudflare 429.
- AWS API Gateway: account, stage/method or usage-plan rate and burst targets cause throttling. Check the most restrictive applicable target before raising only the usage-plan setting. AWS throttling order.
Keep quota counters aligned with response headers
Treat x-ratelimit-remaining-requests and x-ratelimit-reset as provider-defined fields. The name alone cannot tell you whether reset means an epoch timestamp, a duration or a formatted interval. For instance, GitHub’s x-ratelimit-reset is UTC epoch seconds, while its remaining field concerns a particular rate-limit resource.
Track the bucket identity alongside your local counter: credential, endpoint and quota category. Read Retry-After first after a 429, then apply the documented reset rule for the exhausted bucket. A remaining count from one request is a snapshot; concurrent callers can spend it before your next request. Coordinate workers that share a credential rather than giving each worker the full allowance. Keep the server’s response headers in the retry log so you can compare its decisions with your counter.
Fix it as the client
- Stop retrying immediately; a tight loop adds more requests to an already exhausted limiter.
- Read
Retry-After. It is either seconds or an HTTP date, so handle both. - Look at the limit headers (
RateLimit-Remaining/x-ratelimit-remaining,-Reset) to see which bucket you exhausted. Providers often have separate per-minute, per-day and per-endpoint limits. - Retry with exponential backoff plus jitter; cap the attempts.
- Cut the request count: cache, batch, use conditional requests (304 saves response transfer; quota accounting depends on the provider), and lower concurrency.
- Retry only idempotent requests, or POSTs with an idempotency key the API supports.
function retryDelayMs(res, attempt) {
const ra = res.headers.get('retry-after')
if (ra) {
const secs = Number(ra)
if (!Number.isNaN(secs)) return secs * 1000
const at = Date.parse(ra) // HTTP-date form
if (!Number.isNaN(at)) return Math.max(0, at - Date.now())
}
const base = Math.min(30_000, 2 ** attempt * 500)
return Math.random() * base // full jitter
}
// GET only: request bodies need a separate replay and idempotency policy.
async function fetchWithRetry(url, max = 5) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url)
if (res.status !== 429 || attempt >= max) return res
const delay = retryDelayMs(res, attempt)
await res.body?.cancel()
await new Promise((r) => setTimeout(r, delay))
}
}
Inspect what the server tells you:
curl -s -o /dev/null -D - https://api.example.com/v1/items | grep -iE '^(HTTP|retry-after|ratelimit|x-ratelimit)'
Fix it as the site owner
Express (express-rate-limit v7 and later)
The option is limit (it was max before v7). req.rateLimit.resetTime is a Date, not a number:
import { rateLimit } from 'express-rate-limit'
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 100,
standardHeaders: 'draft-7', // RateLimit + RateLimit-Policy; 'draft-6' emits RateLimit-Limit/-Remaining/-Reset
legacyHeaders: false, // drop X-RateLimit-*
handler: (req, res, _next, options) => {
const retryAfter = Math.max(1, Math.ceil((req.rateLimit.resetTime.getTime() - Date.now()) / 1000))
res.set('Retry-After', String(retryAfter))
res.status(options.statusCode).json({ error: 'rate_limited', retryAfter })
}
})
app.set('trust proxy', 1) // for a deployment with exactly one trusted proxy hop
app.use('/api/', limiter)
Behind a reverse proxy without trust proxy, all users appear to come from the proxy IP and hit the limit together. Verify the actual proxy chain before using a hop count: alternate paths with fewer hops can expose a spoofed forwarded address. Use a shared store (Redis) when you run more than one instance.
nginx
http {
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_status 429; # default is 503
server {
error_page 429 @rate_limited;
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://app;
}
# Retry-After only on the rejection, not on every successful response
location @rate_limited {
default_type application/json;
add_header Retry-After 1 always;
return 429 '{"error":"rate_limited"}';
}
}
}
An illustrative rejection log contains limiting requests, excess: 20.450 by zone "api", client: 203.0.113.9. Use limit_req_dry_run on; to measure before enforcing.
Cloudflare and API Gateway
Cloudflare rate limiting rules (Security, Security rules) count requests per characteristic (IP, header, API key) and, with the default block response, return 429 with error 1015. Custom responses can change the page and status. API Gateway throttles with account and per-stage rate and burst limits, and usage plans per API key; raise them in the usage plan or request a quota increase.
Algorithms in one table
| Algorithm | Behavior | Trade-off |
|---|---|---|
| Fixed window | N requests per calendar window | Allows 2N across a window boundary |
| Sliding window | N requests in any trailing window | More state, smoother |
| Token bucket | Refills at rate r, bursts up to bucket size | Allows bursts; API Gateway documents this algorithm |
| Leaky bucket | Constant outflow rate | Queues or rejects bursts; nginx limit_req without nodelay |
Related codes
- 503 Service Unavailable: the server is overloaded or down, not throttling you specifically.
- 403 Forbidden: blocked by policy (WAF, IP ban) rather than by rate.
- 408 Request Timeout: the server gave up waiting on a slow client.
Frequently asked questions
What does 429 Too Many Requests mean?
The client sent more requests than the server, API gateway or CDN allows in a time window. It is defined in RFC 6585, and the response may carry Retry-After saying how long to wait before trying again.
How do I fix a 429 error as an API client?
Stop the request loop, read Retry-After (seconds or an HTTP date) and any RateLimit or X-RateLimit-Reset headers, wait that long, then retry with exponential backoff and random jitter. Reduce concurrency, batch requests and cache responses so you stay under the limit.
What is the Retry-After header on a 429?
It tells the client how long to wait before the next request, either as a number of seconds (Retry-After: 30) or as an HTTP date (Retry-After: Wed, 06 Jan 2027 08:00:00 GMT). Clients must handle both forms.
Are X-RateLimit-* headers a standard?
No. X-RateLimit-Limit, -Remaining and -Reset are a widespread convention used by GitHub, Twitter and others, with differing semantics. The IETF httpapi working group is standardising RateLimit and RateLimit-Policy fields in a draft that is not yet an RFC.
Why does nginx return 503 instead of 429 for rate limiting?
limit_req and limit_conn reject with 503 by default. Add limit_req_status 429; (and limit_conn_status 429;) to return the correct code.
What is Cloudflare error 1015?
Error 1015 "You are being rate limited" is a Cloudflare rate limiting rule blocking your IP or key on a zone with that rule enabled; the response status is 429. The site owner adjusts or exempts it in Security rules; as a visitor, wait for the block period to end.
Sources
- MDN Web Docs: 429 Too Many Requestsdeveloper.mozilla.org
- RFC 6585 Section 4: 429 Too Many Requestsrfc-editor.org
- RFC 9110 Section 10.2.3: Retry-Afterrfc-editor.org
- IETF draft: RateLimit header fields for HTTPdatatracker.ietf.org
- express-rate-limit documentationexpress-rate-limit.mintlify.app
- nginx: ngx_http_limit_req_modulenginx.org
Related
API Rate Limiting: Algorithms, Headers and 429
Rate limit an HTTP API: fixed window, sliding window, token bucket and leaky bucket compared, plus 429, Retry-After, RateLimit headers, nginx and Express.
103 Early Hints
The server sends preliminary response headers to help the client start preloading resources. Learn how 103 Early Hints improves page load performance.
206 Partial Content: Range Requests Explained
206 Partial Content returns only the byte range requested. See a curl transcript, Content-Range, If-Range, video seeking and resumable downloads.
226 IM Used: HTTP Delta Encoding
226 IM Used is the response to a delta-encoded request under RFC 3229. See the A-IM and IM headers, how it works, and why almost nothing implements it.