# HTTP 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.

Source: https://howhttpworks.com/status-codes/429
Last reviewed: 2026-10-05

> **TL;DR:** 429 Too Many Requests means you hit a rate limit. Pause the request loop, honor `Retry-After` in 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; inspect `error.response.status`, `error.response.data` and `error.response.headers`. [Axios constructs this message in `settle`](https://github.com/axios/axios/blob/v1.x/lib/core/settle.js).
- **fetch:** the promise resolves when the HTTP response arrives. `response.status === 429` and `response.ok === false`; check those before reading a success payload. A network failure rejects separately. [MDN documents this distinction](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch).
- **Python requests:** `requests.exceptions.HTTPError: 429 Client Error: Too Many Requests for url: https://api.example.com/resource`. This comes from `response.raise_for_status()`, not from `requests.get()` alone. Save the body before raising if it contains useful diagnostics. [Requests source](https://requests.readthedocs.io/en/latest/_modules/requests/models/#Response.raise_for_status).
- **curl `-f`:** `curl: (22) The requested URL returned error: 429`. `curl -i` shows headers and the error body without fail mode; `--fail-with-body` keeps the body while returning a failing exit code. [curl source](https://github.com/curl/curl/blob/master/lib/http.c), [option documentation](https://curl.se/docs/manpage.html#--fail-with-body).
- **.NET:** `System.Net.Http.HttpRequestException: Response status code does not indicate success: 429 (Too Many Requests).` This is the English message from `EnsureSuccessStatusCode()`; inspect the `HttpResponseMessage` first when you need its body. [Runtime message template](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/Resources/Strings.resx).
- **Spring:** `WebClientResponseException.TooManyRequests` with message `429 Too Many Requests from GET https://api.example.com/resource` for `WebClient.retrieve()`. RestTemplate's default error handler uses `HttpClientErrorException.TooManyRequests`; its message can include the response body. These class names follow the [Spring 6.2 WebClient source](https://github.com/spring-projects/spring-framework/blob/6.2.x/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java) and [HTTP client source](https://github.com/spring-projects/spring-framework/blob/v6.2.0/spring-web/src/main/java/org/springframework/web/client/HttpClientErrorException.java).

## 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
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](https://docs.aws.amazon.com/apigateway/latest/developerguide/supported-gateway-response-types.html) |
| `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 `limit` within `windowMs` and returns 429 by default. Check the key, proxy configuration and shared store; multiple processes need a common counter if the quota is global. [Configuration](https://express-rate-limit.mintlify.app/reference/configuration), [proxy troubleshooting](https://express-rate-limit.mintlify.app/guides/troubleshooting-proxy-issues).
- **Django/DRF:** a configured throttle rejects the request and raises `Throttled` (429). Inspect `DEFAULT_THROTTLE_CLASSES`, `DEFAULT_THROTTLE_RATES` and the view's throttle scope; the built-in throttles use Django's cache backend. [DRF throttling](https://www.django-rest-framework.org/api-guide/throttling/).
- **Laravel:** route `throttle` middleware exceeds its named limiter and returns 429. Check `RateLimiter::for(...)`, the `Limit::perMinute(...)` value and the `by(...)` key; change the key if unrelated users share one bucket. [Laravel routing limits](https://laravel.com/docs/12.x/routing#rate-limiting).
- **ASP.NET Core:** rate limiting middleware returns 429 when configured with `RejectionStatusCode = StatusCodes.Status429TooManyRequests`, or an `OnRejected` callback that sets that status. Inspect the endpoint's policy and partition key; a rejection can occur before the handler runs. [Microsoft rate limiting](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit).
- **nginx:** `limit_req` rejects beyond `burst`; it becomes 429 with `limit_req_status 429`. Look for `limiting requests` and the zone name in the error log. [nginx directives](https://nginx.org/en/docs/http/ngx_http_limit_req_module.html).
- **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](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/4xx-client-error/error-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](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html).

## 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`](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api) 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

1. Stop retrying immediately; a tight loop adds more requests to an already exhausted limiter.
2. Read `Retry-After`. It is either seconds or an HTTP date, so handle both.
3. 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.
4. Retry with exponential backoff plus jitter; cap the attempts.
5. Cut the request count: cache, batch, use conditional requests ([304](https://howhttpworks.com/status-codes/304) saves response transfer; quota accounting depends on the provider), and lower concurrency.
6. Retry only idempotent requests, or POSTs with an idempotency key the API supports.

```javascript
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:

```bash
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:

```javascript
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

```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](https://howhttpworks.com/status-codes/503): the server is overloaded or down, not throttling you specifically.
- [403 Forbidden](https://howhttpworks.com/status-codes/403): blocked by policy (WAF, IP ban) rather than by rate.
- [408 Request Timeout](https://howhttpworks.com/status-codes/408): the server gave up waiting on a slow client.
