429
A rate limiter has decided you are asking too often — a policy statement rather than a protocol one, which is why two services answering 429 may be counting entirely different things.
The same three digits are three different problems depending on which side of the request you are on. Read the block that describes you.
Why you are seeing it
It is usually your own code, in a loop you did not mean to write. The recurring shapes are a `useEffect` whose dependency changes on every render, so one request becomes one request per frame; a retry with no backoff, which turns a single failure into a burst; several components each fetching the same endpoint independently on mount; and a token refresh that fires on every 401, so an expired session produces a stream of requests to the one endpoint most likely to be limited. There is also a case that is not your fault at all: when the limit is keyed to an IP address, an office, a campus or a VPN exit makes hundreds of people look like one very busy client, and your perfectly ordinary usage lands on somebody else's budget.
What to do
Read `retry-after` on the response and honour it exactly — RFC 9110 §10.2.3 allows both a number of seconds and an HTTP-date, so parse both rather than assuming the numeric form. If it is absent, back off exponentially with jitter and a hard cap on attempts, because synchronised retries from many clients simply rebuild the burst that caused the limit. Deduplicate in-flight requests by key so five components asking for the same thing make one call, cache the result for the life of the page, and never retry inside a render. To confirm it is a loop, sort the network panel by time and look for the same URL repeating at intervals nothing in your UI could explain. And treat a 429 on your auth endpoint as evidence of a refresh loop rather than of a busy server.
Why you are seeing it
First decide which side of the limiter you are on, because the two situations share nothing. If you are the client, the trap is shared egress: a fleet of servers calling somebody else's API behind one NAT address is one client to a per-IP limit, so per-user traffic that looks trivial hits the ceiling in aggregate, and scaling out makes it worse rather than better. If you are the limiter, the response is yours and its markings say so. Cloudflare's rate limiting rules let you pick the action, so the same limiter can surface as a 429 or as a block page, and GitHub documents that exceeding its limits may come back as 403 or 429. In nginx the default is more surprising: `limit_req` returns `limit_req_status`, which is 503 unless you change it, and it writes `limiting requests, excess: ... by zone ...` to the error log — so a 503 with that log line is a rate limit under another number.
What to do
If you are the client: put one shared limiter in front of the outbound call rather than one per process, key it the way the provider keys it, and read the provider's remaining and reset headers so you learn the budget instead of discovering it by exhausting it. If you are the limiter: read the zone and the key in your config before you raise any number, because a limiter keyed on `$binary_remote_addr` behind a proxy sees the proxy's address and throttles the entire internet as one client unless the real client address is restored first. Set `limit_req_status 429` so clients can tell rate limiting from an outage, and send `Retry-After` even though the RFC only suggests it — it is the difference between a client that waits and a client that hammers. Then check the error log for the zone name, which tells you which rule fired.
Why you are seeing it
The site is saying it has seen too many requests recently from you, from your network, or from an app acting on your behalf, and it wants a pause. It is usually not something you did on purpose: an extension refreshing in the background, several tabs left open on the same site, a page that reloads itself, or a shared office, campus or VPN address where the site counts everyone behind it as a single visitor. It is temporary by design — this is a site protecting itself from load, not a fault with your device and not a ban.
What to do
Wait, and resist reloading. Most limits clear in seconds to a few minutes, and repeated reloads keep the counter topped up, which is why hammering refresh makes it last longer rather than shorter. Close duplicate tabs of the same site, pause any extension or script that polls it, and if you are on a VPN or a shared network try again without it, since the limit may be counting everyone on that address together. If the page names a waiting time, that number is the site's own answer and it is the shortest reliable one you will get. If it never clears, that is worth reporting.
Run this against the URL that failed. It prints the status without the error page, so you can see what the server said rather than what the browser rendered.
curl -sS -o /dev/null -w '%{http_code} %header{retry-after}\n' 'https://example.com/api/health?n=[1-50]'The square brackets are curl's URL globbing, so this is fifty sequential requests to one resource with a changing query string that defeats caching, and each prints one line. `%header{retry-after}` needs curl 7.84 or newer and prints that response header, or nothing when the server did not send one. The line you care about is the transition: the request number where 200 becomes 429 is the burst the limiter permits, and running the command again immediately tells you whether the window is fixed or sliding — an instant second 429 means the window has not rolled, while a fresh allowance means it has. An empty `retry-after` column is a finding in itself, because RFC 6585 makes that header a MAY, and its absence means every client is left to guess the wait. Watch for 503 in that column as well: nginx's `limit_req` answers 503 by default, so a run that switches to 503 at a consistent request number is still a rate limit. To find the sustained rate rather than the burst size, add `--rate 10/s` (also curl 7.84 or newer), which paces the series, and lower the number until the 429s stop.
429 is not in RFC 9110 at all: it comes from RFC 6585 §4, which defines it as the user having sent too many requests in a given amount of time, and says the response representation SHOULD explain the condition and MAY include a `Retry-After` header saying how long to wait. Both of those are deliberately soft, and the section that follows is softer still — RFC 6585 states outright that it does not define how the origin server identifies the user, nor how it counts requests. That single sentence is why this code behaves so differently from service to service: one API counts requests per token, another per IP address, another per endpoint or per account, and none of them is violating anything. It also means the number in front of you is a statement about a policy you cannot read from the response, so guessing the window from one 429 is guesswork. `Retry-After` itself is defined in RFC 9110 §10.2.3 and has two syntaxes a client must handle — a delay in seconds, or an HTTP-date — and since RFC 6585 makes it a MAY rather than a MUST, plenty of limiters send neither. There is no header for the remaining budget that everyone agrees on either; the de-facto set is `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`, and GitHub, for example, documents its `x-ratelimit-reset` as a UTC epoch in seconds, so it needs converting before it means anything. Two more facts save time. 429 is not among the heuristically cacheable codes in RFC 9110 §15.1, so a cache will not hold one on its own. And the code is not the only way a limiter says no: nginx's `limit_req` module answers with `limit_req_status`, which defaults to 503, so a great deal of real rate limiting never appears as a 429 at all.
| Confused with | How to tell them apart |
|---|---|
| 503 | The most expensive mix-up in this pair is that nginx's own rate limiter answers 503 rather than 429: `limit_req_status` defaults to 503, so a load-related 503 with `limiting requests, excess: ... by zone ...` in the error log is a limit and not an outage. Both codes may carry `Retry-After`, so the header does not separate them — the log line and whether the failure tracks request volume do. |
| 403 | A limiter is free to refuse with something other than 429. GitHub documents that exceeding its rate limits can return 403 or 429, and Cloudflare's rate limiting rules let the configured action be a block instead. So a 403 that appears only under load and clears after a wait is a limit rather than a permission problem, and auditing roles will find nothing. |
| 401 | These two produce each other. A client that refreshes its token on every 401 turns one expired session into a burst against the auth endpoint, which is the endpoint most likely to be rate limited, and the resulting 429 then looks like an unrelated outage. If 429s are concentrated on your token endpoint, the bug is the refresh loop in the client, not the limit on the server. |
Retry-After | RFC 6585 makes this header a MAY on a 429, so its absence is legal and common, and RFC 9110 §10.2.3 gives it two forms: a delay in seconds or an HTTP-date. A client that parses only the numeric form silently treats a dated header as zero and retries immediately, which is the fastest way to convert one 429 into a longer block. |
Cloudflare | When the limit is applied at the edge, the response is written by Cloudflare rather than by your origin, and your application logs will show nothing at all for those requests — which is exactly what makes it confusing. A branded page carrying a `cf-ray` value is the tell, and that ray id is what support can look up; the rate limiting rule's own configuration decides whether the visitor sees a 429 or a block. |
All of these run in your browser — nothing is uploaded.
Codes people usually end up reading in the same session as 429.
Read the response headers rather than the page. curl -sS -o /dev/null -D - https://example.com/path prints them without the body, and server, via and cf-ray between them name the layer that answered. A cf-ray value means Cloudflare handled the response and is the id their support will ask for. To take the edge out of the picture entirely, repeat the request with --resolve example.com:443:203.0.113.10, which connects to the origin address you name while still sending the original host name and SNI — if the answer changes, the edge and the origin disagree.
No, and you should never branch on it. RFC 9112 tells clients to ignore the reason phrase, servers are free to change it, and HTTP/2 and HTTP/3 do not carry one at all — so 404 Not Found over HTTP/1.1 arrives as a bare 404 over HTTP/2. The phrases on this site are the registered ones because they are what people search for and what appears in an HTTP/1.1 log, not because any software depends on them.
Retry 5xx and 429; do not retry 4xx, because nothing about the request will be different next time. If the response carries Retry-After, honour it — RFC 9110 defines it for exactly this, and it may be either a number of seconds or a date. Otherwise use exponential backoff with jitter and a hard cap, and only for idempotent methods: a retried POST can charge a card twice. A retry storm against a server that is already failing is how a brief incident becomes a long one.
Yes, and the only rule that matters is that the code has to be true. Anything that reads your responses automatically — search engines, monitoring, caches, client retry logic — makes decisions from the number and never from the page. Serving an error page with 200 hides the failure from your own alerting; serving 200 for a missing page gets the error indexed as content; returning 500 for a request that was simply malformed sends whoever is on call to the wrong half of the stack.
Still stuck on 429? browse every status code in the reference — or go back up to the block written for your side of the stack.