ByteScope

504

How to fix HTTP 504 Gateway Timeout

5xx server errorIETF standardRFC 9110

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.

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
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 withHow to tell them apart
502Both 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.
524524 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.
503503 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.
nginxThe 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 504AWS 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.

Related status codes

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

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 504? browse every status code in the reference — or go back up to the block written for your side of the stack.