503
Something decided not to serve you, on purpose and for now — a maintenance page, a rate limiter or a load balancer with nothing healthy to send you to — which makes 503 the only 5xx that comes with an expiry date.
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 arrives all at once and everywhere: every endpoint, often the HTML document too, and usually with an error page that looks nothing like your application because your application did not write it. The tell that separates it from a real outage is the `Retry-After` header, which says the refusal is deliberate and time-boxed. The trap is the retry loop you already have — a client that treats 5xx as "try again immediately" turns a limiter into a self-inflicted denial of service and a maintenance window into a thundering herd the moment it ends.
What to do
Read `Retry-After` before deciding anything, and honour it: it is either a number of seconds or an HTTP-date, both defined in RFC 9110 §10.2.3, so parse both shapes. When it is absent, back off exponentially with jitter and a hard cap on attempts, because every client waking on the same round number is what stops a recovering service from recovering. Show the user a specific state — unavailable, retrying shortly — rather than a generic failure, since a 503 is expected to end and a generic error invites a page reload that costs another request. Treat writes carefully even here: a 503 usually means the request was refused before it was processed, but the status alone cannot prove that, so anything that charges or creates should carry an idempotency key rather than rely on the code.
Why you are seeing it
Three different layers produce 503 and they are indistinguishable from outside. It may be deliberate — a maintenance `return 503` or a feature flag shedding load. It may be a limiter, and in nginx that is the default: `limit_req_status` and `limit_conn_status` are documented as 503, so your rate limiting is reported as an outage rather than as rate limiting. Or it may be a health decision above the application — an Application Load Balancer with no healthy targets, which AWS documents as insufficient targets ready to receive requests, or Apache's `mod_proxy` holding a worker in an error state for the `retry` window, 60 seconds by default.
What to do
Find which layer answered before touching anything, and the access logs do that for you by omission: the layer that logged the request is at or above the refusal, and the first layer with no record of it is below. A 503 that never reaches your application log was written by the proxy, the limiter or the balancer, and reading application code is wasted time. Once you know the layer, the question is different for each: for a limiter, whether the rate is right and whether `limit_req_status 429` would be the more honest answer; for a balancer, what the health check is actually requesting and whether it fails for a reason the real traffic does not share — a health endpoint that touches the database fails an outage the rest of the app could have ridden out. Always set `Retry-After` on a refusal you chose, because it is the only thing that lets a well-behaved client wait instead of hammer. And check the recovery window: with Apache's default `retry` of 60 seconds, a fixed backend keeps returning 503 for up to a minute after it is healthy, which is long enough to convince you the fix did not work.
Why you are seeing it
The site is switched on but is deliberately not answering right now. That is different from a broken site: somebody is either doing maintenance, or the site is under more load than it can serve and is turning people away to protect itself. The page often says roughly how long, and it is usually minutes rather than hours. It affects everyone, so it is not about your account, your device or your connection.
What to do
Wait for the time the page suggests, then reload once. Repeatedly refreshing is actively counterproductive when the cause is overload, because every reload is another request the site is already unable to serve. Do not clear cookies, reinstall the app or reset your router — none of them touch the cause. If you were part-way through something that costs money, check whether it went through before starting again. And if there is no indication of a time and it lasts beyond a few minutes, the site's status page or social account is where the answer will appear first, since the operators already know.
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 -D - -w '\n%{http_code} retry-after=%header{retry-after} in %{time_total}s\n' https://example.com/api/`-D -` prints the response headers and `%header{retry-after}` pulls that one field into the summary line; it is a documented `--write-out` variable in current curl, and on a build old enough not to have it the `-D -` dump still shows the header. What the output tells you comes in three parts. A `retry-after` with a value means the refusal is deliberate and somebody chose a duration, which is the fastest way to distinguish planned maintenance or a limiter from a load balancer that simply has nothing healthy behind it; an empty one means whatever refused you ignored RFC 9110 §15.6.4's suggestion and any wait you pick is a guess. The `server:` header names the layer that wrote the page, and that is the layer whose configuration to read. The elapsed time separates a refusal from a failure: a limiter or a maintenance rule answers in milliseconds because no upstream was contacted at all. To find out whether a limiter is the cause, repeat the request faster than the configured rate — `for i in $(seq 1 50); do curl -sS -o /dev/null -w '%{http_code} ' https://example.com/api/; done` — and watch the statuses flip from 200 to 503 partway through the row, which is a limiter's signature and nothing else's. Adding `--resolve example.com:443:203.0.113.10` connects straight to the origin address while still sending the original host name and SNI, so a 503 that vanishes came from the edge and a 503 that survives came from the origin.
RFC 9110 §15.6.4 defines 503 as the server being currently unable to handle the request due to a temporary overload or scheduled maintenance, which will likely be alleviated after some delay, and says the server MAY send a `Retry-After` header field to suggest how long the client should wait. That header is what makes 503 structurally different from every other 5xx: §10.2.3 defines `Retry-After` as either a number of seconds or an HTTP-date, and when sent with a 503 it states how long the service is expected to be unavailable. The word MAY is doing work there: a 503 with no `Retry-After` is entirely conformant, so an absent header tells you nothing beyond the fact that whoever refused you declined to say when to come back — and a server that has run out of capacity to accept connections at all produces a connection error rather than a 503, so an absence of 503s on a dashboard is not evidence that nothing was overloaded. Two more properties matter while you are looking at one. 503 is not among the heuristically cacheable codes RFC 9110 §15.1 names — 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414 and 501 — so nothing stores it without being told to, and a 503 that outlives its cause is being regenerated rather than replayed. And 503 says nothing about your request in particular — it is a statement about the service, which is precisely the line separating it from 429. Where it actually comes from is rarely application code. nginx's rate limiters default to it: `limit_req_status` and `limit_conn_status` are both documented with a default of 503, so a request rejected for exceeding a configured rate arrives as a 503 unless somebody changed the directive. Apache's `mod_proxy` documents that a backend which fails puts the connection-pool worker into an error state, after which httpd "will not forward any requests to that server until the timeout expires" — the `retry` parameter, 60 seconds by default — so a refusal there can outlive the restart that fixed it. And AWS documents that consistent HTTP 503s from an Application Load Balancer mean there are insufficient targets ready to receive requests, which is a health-check outcome rather than an application fault.
| Confused with | How to tell them apart |
|---|---|
| 429 | 429 (RFC 6585 §4) says you specifically are sending too many requests; 503 says the service is unavailable to everybody. The reason they blur is a default: nginx documents `limit_req_status` and `limit_conn_status` as 503, so most rate limiting on the internet is reported as an outage. If you operate the limiter, set it to 429 — clients can then back off the one caller instead of assuming the whole service is down, and your dashboards stop counting throttling as downtime. |
| 502 | The same underlying event can produce either code depending on which proxy you run, which is why the number alone is a poor diagnosis. nginx reports a backend it cannot get a valid response from as 502, while Apache's `mod_proxy` takes the failed worker out of rotation and refuses to forward to it for the `retry` window (60 seconds by default), so the refusal is what the client sees. Read it as: 502 means something failed to answer, 503 means something declined to ask. |
| 500 | 500 is an unexpected condition and 503 is an expected one. RFC 9110 gives 503 a `Retry-After` channel and gives 500 nothing, because there is nothing sensible to say about when an unhandled exception will stop happening. A service that sheds load by throwing exceptions is reporting a planned decision as a crash, and every client that keys its backoff on 503 will retry it immediately. |
nginx | In nginx a 503 is almost never the application's. `limit_req` and `limit_conn` both default to it, a maintenance block is usually a literal `return 503`, and the error log records rate-limit refusals at the level `limit_req_log_level` sets, with delays logged one level lower than refusals. Grepping that log for the limiting zone name tells you in one line whether you are looking at a limiter or at something else wearing the same three digits. |
Retry-After | RFC 9110 §10.2.3 defines the field in two forms — a non-negative number of seconds, or an HTTP-date — and a client that parses only one of them silently mishandles half the servers in the world. Sent with a 503 it says how long the service expects to be unavailable; sent with a 3xx it means something different, the minimum time before issuing the redirected request. Parse both shapes, and treat a missing header as "unknown", never as "retry now". |
All of these run in your browser — nothing is uploaded.
Codes people usually end up reading in the same session as 503.
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 503? browse every status code in the reference — or go back up to the block written for your side of the stack.