ByteScope

503

How to fix HTTP 503 Service Unavailable

5xx server errorIETF standardRFC 9110

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.

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
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 withHow to tell them apart
429429 (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.
502The 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.
500500 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.
nginxIn 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-AfterRFC 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.

Related status codes

Codes people usually end up reading in the same session as 503.

Frequently asked questions

How do I find out which server produced this status code?

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.

Does the reason phrase after the number matter?

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.

Should I retry a request that failed with this code?

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.

Can I make my server return a different status code?

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.