503
How to fix HTTP 503 Service Unavailable
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.
Why you are seeing 503, and what to do about it
The same three digits are three different problems depending on which side of the request you are on. Read the block that describes you.
Front-end developer
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.
Back-end developer
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.
End user
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.
Reproduce 503 with curl
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.
What 503 actually means
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.
Codes 503 gets mistaken for
| 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". |
- Reason phrase
- Service Unavailable
- Class
- 5xx server error
- Defined by
- RFC 9110
- Standard
- IETF standard
Tools on this site
All of these run in your browser — nothing is uploaded.
- Port Number Reference — confirm which service is meant to be answering on the port a health check is probing, when the balancer says nothing is healthy
- Subnet Calculator — check that the health checker's source range is inside the range your security group or firewall actually allows
- Certificate Decoder — read the certificate a health check is failing on, when targets go unhealthy at the moment a certificate rotated
Related status codes
Codes people usually end up reading in the same session as 503.