504
How to fix HTTP 504 Gateway Timeout
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.
Why you are seeing 504, 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
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.
Back-end developer
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.
End user
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.
Reproduce 504 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 - --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.
What 504 actually means
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.
Codes 504 gets mistaken for
| 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. |
- Reason phrase
- Gateway Timeout
- 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.
- PCAP Viewer — open a capture between the proxy and the upstream and see whether the response ever started, or the connection simply sat idle until the timer fired
- Port Number Reference — check what should be answering on the upstream port, when the log says the timeout happened while connecting rather than while reading
- Certificate Decoder — inspect the upstream certificate when a connect timeout turns out to be a TLS handshake that never completed
Related status codes
Codes people usually end up reading in the same session as 504.