504 Gateway Timeout
Server error response defined by RFC 9110 §15.6.5.
Last updated July 27, 2026
Common causes at a glance
- Proxy read timeout shorter than the upstream's real processing time
- A slow database query or third-party API call inside the request path
- Upstream worker pool exhausted, so requests queue before they start
- Timeouts configured in the wrong order across CDN, load balancer, and origin
Reproduce it
curl -sS -o /dev/null -w 'code=%{http_code} t=%{time_total}\n' https://www.example.com/reports/bigWhat 504 Gateway Timeout means
Every 504 is a disagreement between two timeouts. A request passes through a chain — browser, CDN, load balancer, reverse proxy, application server, database — and each hop has its own patience. Whichever is shortest decides the outcome. A 504 means an intermediate hop’s limit fired while the hop below it was still working, so the work continues, invisibly, after the client has been told it failed.
The near-universal reflex is to raise the timeout on whichever box emitted the 504. That is usually wrong. Raising one number moves the failure to the next hop up, which now times out instead, while you hold connections open for a request nobody is waiting for. Write down every timeout in the path and make them decrease as you move toward the client, so the hop closest to the slow work gives up first and can say something specific about why. Then make the slow endpoint asynchronous.
What the spec says
RFC 9110 §15.6.5 defines 504 as a gateway or proxy not receiving a timely response from an upstream server it needed to access. “Timely” is deliberately undefined: the specification sets no duration and leaves the threshold to the intermediary. The contrast with §15.6.3 is the useful part — 502 is an invalid response, 504 is no response in time. Nothing makes 504 retryable, and blindly retrying a non-idempotent request that timed out is how duplicate orders get created.
What actually causes it
The endpoints that generate 504s are predictable: report generation, bulk exports, search across a large index, anything calling a payment or shipping provider synchronously. A 504 whose total time lands on a suspiciously round number — 30, 60, 120 seconds — is showing you the timeout value itself, and that number identifies the hop that gave up. In nginx the relevant knobs are proxy_connect_timeout and proxy_read_timeout, whose 60-second read default is why so many 504s arrive at exactly one minute. Worker exhaustion produces the same symptom with no slow query at all.
How to debug it
Measure first. Elapsed time before the 504 names the hop that gave up:
curl -sS -o /dev/null -w 'code=%{http_code} connect=%{time_connect} total=%{time_total}\n' \
https://www.example.com/reports/big
Match that total against each configured timeout in the chain. Then run the same request directly against the origin with a generous client timeout: if the origin finishes in 90 seconds and the proxy waits 60, you have your answer without touching any code. Check the upstream’s logs at that timestamp — a 200 logged by the application for a request the client saw as 504 confirms the work was done and only the reporting was lost. Build a repeatable probe with the curl builder.
Working examples
curl
# The total time before the 504 usually equals the offending hop's timeout.
curl -sS -o /dev/null \
-w 'code=%{http_code} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n' \
https://www.example.com/reports/big
# How long does the origin really take? Give the client plenty of rope.
curl -sS -o /dev/null --max-time 300 \
-w 'origin code=%{http_code} total=%{time_total}\n' \
http://10.0.1.24:8080/reports/big
Python (requests)
import requests
# (connect timeout, read timeout). Set the read timeout well above the
# gateway's so you can see whether the origin eventually answers at all.
try:
resp = requests.get("http://10.0.1.24:8080/reports/big", timeout=(3, 300))
print("origin finished in", resp.elapsed.total_seconds(), "s:", resp.status_code)
except requests.exceptions.ReadTimeout:
print("origin itself never answered; the proxy is not the problem")
edge = requests.get("https://www.example.com/reports/big", timeout=(3, 300))
print("edge:", edge.status_code, edge.elapsed.total_seconds())
Node (fetch)
// Time the edge, then the origin, and compare against configured timeouts.
async function timed(url, ms) {
const t0 = performance.now();
const secs = () => ((performance.now() - t0) / 1000).toFixed(1);
try {
const res = await fetch(url, { signal: AbortSignal.timeout(ms) });
return `${res.status} in ${secs()}s`;
} catch {
return `aborted after ${secs()}s`;
}
}
console.log("edge :", await timed("https://www.example.com/reports/big", 120000));
console.log("origin:", await timed("http://10.0.1.24:8080/reports/big", 300000));Reference and tooling
The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.
Other 5xx codes
- 500 Internal Server Error
- 501 Not Implemented
- 502 Bad Gateway
- 503 Service Unavailable
- 505 HTTP Version Not Supported
- 506 Variant Also Negotiates
- 507 Insufficient Storage
- 508 Loop Detected
- 510 Not Extended
- 511 Network Authentication Required
- 520 Web Server Returns an Unknown Error
- 521 Web Server Is Down
- 522 Connection Timed Out
- 523 Origin Is Unreachable
- 524 A Timeout Occurred
- 525 SSL Handshake Failed
- 526 Invalid SSL Certificate
- 530 Unable to Resolve Origin Hostname