504
A proxy waited for an upstream, ran out of patience and answered on its behalf — so the number that identifies the culprit is not the status code but the elapsed time, which is always somebody's configured timeout.
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
The request does not fail, it hangs — a spinner that runs for about a minute and then resolves into an error page nobody designed. It is one endpoint, usually a report, an export, a search across everything or an upload, and it gets worse with the size of what was asked for, which is the giveaway that a timer rather than a bug is involved. If you set your own `AbortController` deadline you may never see the 504 at all: your abort fires first and the failure is reported as a network error, which sends the investigation to the wrong place.
What to do
Measure before you theorise. The network panel's timing column, or `performance.getEntriesByName(url)`, gives you the elapsed time, and a round number — 60 seconds, 30, 120 — is not a coincidence, it is the answer. Then set an explicit client timeout deliberately shorter than the server's, so you control the message the user sees instead of inheriting a proxy's HTML page, and log the elapsed time alongside the failure so the back end gets the timer rather than a screenshot. Do not retry a POST: the upstream may have finished the work after the proxy stopped listening, so a retried export becomes two exports and a retried payment can become two payments. If the endpoint genuinely needs minutes, it needs a different shape — the request returns a job id immediately and the client polls for it — and no timeout setting anywhere will make the long request reliable across every proxy and mobile network between you and the server.
Why you are seeing it
One of your timers expired, and nginx writes down which one. `upstream timed out (110: Connection timed out) while reading response header from upstream` is `proxy_read_timeout` — the connection was made, the request was sent, and the upstream never started answering. The same message ending `while connecting to upstream` is `proxy_connect_timeout`, which is a network or listener problem rather than a slow application, and `while sending request to upstream` is `proxy_send_timeout`. All three default to 60 seconds. The log line also names the upstream address, so a group where only one member times out is a sick instance rather than a slow endpoint.
What to do
Read the error log line first: it names the timer and the upstream, and those two facts remove most of the search space before you open any application code. Then get the upstream's own view — if the application logs request durations, the request that produced the 504 is usually still in there, logged with a duration longer than the proxy's timeout, which proves the work completed after the proxy left and tells you exactly how long it really takes. If the request never appears in the application log at all, nothing was slow: the request was queued in front of the application, which is a saturated worker pool and a capacity problem rather than a query to optimise. Before raising any timeout, check `proxy_next_upstream`: it defaults to `error timeout`, so one slow upstream multiplies the client's wall clock by the number of servers in the group, and `proxy_next_upstream_timeout` and `proxy_next_upstream_tries` are the two bounds that stop that. Raising the timeout is a last resort and moves the failure rather than fixing it — every layer between the browser and your application has its own timer, and the shortest one always wins.
Why you are seeing it
The page tried to load for about a minute and then gave up. That means the site is running but one part of it is taking far longer than the system is willing to wait — commonly a search, a report, a large download or an upload. The site may be busy, or the specific thing you asked for may simply be big. It is not your connection: a slow connection produces a page that loads gradually, not one that stops dead on a round number.
What to do
Wait a minute and try once more, and if it is a search or a report, ask for less — a shorter date range, fewer results, one file instead of a folder — because the amount you asked for is often the whole difference between an answer and a timeout. If you were uploading or submitting something, check before repeating it: a timeout hides the answer but does not undo the action, so the upload may have finished on the site's side even though your browser never heard back. Look at the account page, order history or inbox first. If the same request times out repeatedly, that is worth reporting with the exact time and what you asked for, since the operators can find the slow request in their logs from those two details.
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 - --max-time 180 -w '\n%{http_code} connect=%{time_connect}s ttfb=%{time_starttransfer}s total=%{time_total}s\n' https://example.com/slow`--max-time 180` sets curl's limit on the whole operation and it has to be comfortably longer than the server's timer, otherwise curl aborts first and you learn nothing except that you were impatient. The three timings are the diagnosis. `%{time_connect}` is when the TCP connection completed, `%{time_starttransfer}` is when the first byte arrived, and `%{time_total}` is the end; a 504 has a fast connect and a total that lands on a round number. Match that number to a documented default and you have the layer: about 60 seconds is nginx's `proxy_read_timeout` or an Application Load Balancer's idle timeout, both of which default to 60, while about 125 seconds is Cloudflare's read timeout — and that one is reported as 524, not 504, so a 504 at 125 seconds is not Cloudflare's. A total noticeably shorter than any default means somebody set the timer deliberately, which narrows it to a configuration file rather than a product. Then take the layers apart: `--resolve example.com:443:203.0.113.10` connects straight to the origin address while still sending the original host name and SNI, so if the origin answers in 90 seconds while the edge gave up at 60, the work is genuinely slow and the timer is merely the messenger. Watch the origin's own access log while the command runs — a request that appears there with a duration longer than the timeout is proof the work completed after the proxy stopped waiting, which is exactly why retrying a write here is dangerous.
RFC 9110 §15.6.5 defines 504 as the status a server returns while acting as a gateway or proxy when it did not receive a timely response from an upstream server it needed to access in order to complete the request. Read next to §15.6.3, which reserves 502 for an invalid response, that single word "timely" is the whole distinction and it has a stopwatch attached: 502 comes back in milliseconds because something refused or broke, while 504 comes back on a round number because a timer expired. The round numbers are documented and they identify the layer. nginx defaults `proxy_read_timeout`, `proxy_connect_timeout` and `proxy_send_timeout` to 60 seconds each, and notes that the connect timeout cannot usually exceed 75 seconds because the operating system's own connection attempt ends there. AWS documents an Application Load Balancer's connection idle timeout as 60 seconds by default, configurable from 1 to 4000, and advises sending at least one byte before each idle period elapses so a long upload is not cut. Cloudflare's own read timeout is 125 seconds and produces 524 rather than 504. So a 504 at roughly one minute is a default nginx or ALB timer, and a 504 at some other round number is a timer somebody set on purpose. Two properties change what you do next. 504 is not one of the status codes RFC 9110 §15.1 defines as heuristically cacheable, so it is regenerated per request rather than stored by an intermediary that was told nothing. And unlike 502, a 504 carries no evidence at all about whether the work happened: the upstream was still holding the connection when the proxy gave up, so it may well have completed the request a second later and written everything it was asked to. That makes a blind retry of a non-idempotent request the most expensive mistake available here. nginx encodes the same caution — `proxy_next_upstream` defaults to `error timeout`, so a timeout does move to the next server in the group, but requests with a non-idempotent method are not passed on once they have been sent unless `non_idempotent` is added explicitly, a parameter that exists since nginx 1.9.13.
| Confused with | How to tell them apart |
|---|---|
| 502 | Both are an intermediary reporting on an upstream and RFC 9110 draws the line precisely: §15.6.3 is an invalid response, §15.6.5 is no timely response. The clock settles it faster than the wording. A 502 comes back in milliseconds because the connection was refused or dropped; a 504 comes back on a timer. The consequence matters more than the label: after a 502 the request almost certainly did nothing, while after a 504 the upstream may have completed it. |
| 524 | 524 is Cloudflare's own code, not an IETF one, for exactly this situation at exactly one place: Cloudflare connected to the origin and no HTTP response arrived within its 125-second read timeout. So a 504 arriving through Cloudflare was produced by something else — your own proxy, or an origin acting as a gateway — and relayed. If you want the timeout raised, note that only Enterprise zones can change Cloudflare's, and only up to 6,000 seconds. |
| 503 | 503 is a refusal made in advance and 504 is a wait that ran out. RFC 9110 §15.6.4 lets 503 carry `Retry-After` because the server knows roughly when it will be back; a 504 has nothing equivalent, because the proxy has no idea what the upstream is doing. If your load shedding produces 504s, it is not shedding load — it is queueing requests until a timer kills them, which costs the upstream the work anyway. |
nginx | The error log names the timer, and the three messages are worth knowing apart. `while connecting to upstream` is `proxy_connect_timeout` and points at the network or a missing listener; `while reading response header from upstream` is `proxy_read_timeout` and points at a slow application; `while sending request to upstream` is `proxy_send_timeout` and usually means a large request body against a stalled reader. All three default to 60 seconds, and the connect timeout cannot usually exceed 75. |
ELB 504 | AWS documents an Application Load Balancer's connection idle timeout at 60 seconds by default, adjustable from 1 to 4000, and a 504 when the target does not respond within it. Their own advice for long operations is to send at least one byte before each idle period elapses, which is why a streamed response survives where a silent one dies. Check the target's keep-alive as well: AWS notes it should not be shorter than the load balancer's idle timeout. |
All of these run in your browser — nothing is uploaded.
Codes people usually end up reading in the same session as 504.
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 504? browse every status code in the reference — or go back up to the block written for your side of the stack.