Understanding HTTP 504 Gateway Timeout: Who Gave Up Waiting, and Why
504 Gateway Timeout is the status code of unanswered knocks. A proxy — NGINX, a load balancer, an API gateway — forwarded your request to a backend, started a timer, and the timer ran out before the backend said anything. Here's the part that trips people up: a 504 is not your application reporting that it's slow. Your application probably never got to report anything — the middleman gave up on its behalf. That also means 504 is an infrastructure code: if your own application code is timing out waiting for a database, you generally want 500 or 503, not 504. In this post we'll pin down what 504 means, untangle it from 502, 503, 408, and 499, look at where 504s actually come from (NGINX, AWS load balancers, Cloudflare), and go through the timeout-alignment strategy that makes them stop.
What Is a 504?
A 504 means the server you reached was acting as a gateway or proxy, and the server behind it didn't respond in time.
The 504 (Gateway Timeout) status code indicates that the server, while acting as a gateway or proxy, did not receive a timely response from an upstream server it needed to access in order to complete the request. — RFC 9110, Section 15.6.5
In plain English: "I'm the middleman. I asked the backend for you, I waited as long as I'm configured to wait, and it never answered — so I'm giving up." The proxy has a timeout (commonly 60 seconds), the upstream blew through it, and the proxy synthesized this response itself. The backend may still be happily working on your request; the proxy just stopped listening.
The raw response is minimal:
HTTP/1.1 504 Gateway TimeoutThe Key Question: Who Gave Up Waiting?
Every request passes through a chain — browser → CDN → load balancer → app server → database — and every hop has its own timer. Most of the "timeout family" of status codes are just answers to one question: whose timer expired first?
- 504 — the proxy gave up waiting for the upstream server. The middleman's timer fired.
- 408 Request Timeout — the server gave up waiting for the client to finish sending its request. The problem is on the way in, not the way out.
- 499 Client Closed Request (NGINX's non-standard code) — the client gave up and disconnected before the server answered. Nobody sends 499 over the wire; NGINX logs it.
- Cloudflare 524 A Timeout Occurred — Cloudflare's own flavor of 504: it connected to your origin fine, but the origin didn't produce an HTTP response within Cloudflare's limit (100 seconds by default).
A memorable way to keep the core trio straight:
- 504 = "The backend never answered." Silence past the deadline.
- 502 = "The backend answered, but the answer was garbage." A response arrived — it was invalid, malformed, or the connection was refused/reset.
- 503 = "The backend answered: 'not right now.'" The service itself is telling you it's overloaded or down for maintenance.
With a 502, the proxy got something and couldn't use it. With a 504, the proxy got nothing before its timer expired. That single distinction — bad response vs. no response — is the whole difference, and it points you at completely different fixes: a 502 sends you looking for a crashed or misbehaving process, while a 504 sends you looking at slow work and mismatched timeouts.
504 vs 502 vs 503 vs 408
| Code | Meaning | Use when |
|---|---|---|
| 504 Gateway Timeout | Upstream didn't respond in time | The proxy's read timeout expired with no response from the backend |
| 502 Bad Gateway | Upstream sent an invalid response | The backend crashed mid-response, returned garbage, or refused the connection |
| 503 Service Unavailable | Service is temporarily unable to serve | Overload, maintenance mode, or a health check took the backend out of rotation |
| 408 Request Timeout | Client was too slow sending the request | The server timed out waiting for the request itself to arrive |
408 is the mirror image of 504 and the one most often misapplied: 408 blames the client's upload, 504 blames the backend's response. If your API handler times out waiting on a slow database query, neither code fits — the HTTP request arrived just fine and you're not a gateway. Return a 500 (unexpected failure) or 503 (known overload) instead.
Common Causes
Real-world 504s come from a short list of suspects:
- The upstream is genuinely slow. An unindexed database query, a cold cache, an N+1 query storm, a report endpoint doing 30 seconds of work behind a proxy that allows 10. The proxy is behaving correctly; the work is just too slow.
- Timeout misalignment. The proxy's timeout is shorter than the time the backend legitimately needs. Nobody's broken — the timers just disagree. This is the most fixable cause and the most common.
- The upstream is wedged, not dead. A process stuck on a lock, an exhausted connection pool, an event loop blocked by synchronous work. The port still accepts connections (so no instant 502) but nothing ever comes back — the worst failure mode, because every request burns the full timeout before failing.
- Network trouble between servers. Packet loss or routing problems between the proxy and the upstream, a security group or firewall silently dropping packets instead of refusing them. Dropped packets look exactly like a slow server: silence.
- DNS delays inside the chain. The proxy resolves the upstream hostname slowly or gets a stale record pointing somewhere unresponsive.
- Cascading waits. Your API gateway calls service A, which calls service B, which is slow. Whichever hop has the shortest timer fires first, and the 504 surfaces at a layer far from the actual slowness.
Where 504s Come From (and How to Tune Them)
You rarely write a 504 — you configure it. Each piece of infrastructure has its own timer and its own dial.
NGINX
NGINX returns 504 when an upstream exceeds one of its proxy timeouts — all three default to 60 seconds:
location /api/ {
proxy_pass http://backend:3000;
# Time allowed to establish the TCP connection to the upstream
proxy_connect_timeout 5s;
# Time allowed between successive reads of the response.
# This is the timer behind most NGINX 504s.
proxy_read_timeout 60s;
# Time allowed between successive writes of the request
proxy_send_timeout 60s;
}A subtlety worth knowing: proxy_read_timeout isn't a cap on the total response time — it's the maximum gap between two successive reads. An upstream that streams a byte every few seconds can run far longer than 60 seconds without tripping it; an upstream that goes silent for 60 seconds trips it immediately. If one known-slow endpoint (an export, a report) needs more time, raise the timeout in a location block scoped to that path — don't raise it globally and let every broken endpoint hang clients for five minutes.
AWS Application Load Balancer
An ALB returns 504 when the target doesn't respond within its idle timeout, which defaults to 60 seconds. The classic ALB gotcha is keep-alive misalignment: the ALB reuses connections to your targets, and if your app server's keep-alive timeout is shorter than the ALB's idle timeout, the app can close a connection the ALB was about to reuse. The standing rule: keep your application's keep-alive timeout longer than the load balancer's idle timeout. For Node.js behind an ALB:
const server = app.listen(3000);
// ALB idle timeout is 60s — keep the app's keep-alive longer than that
server.keepAliveTimeout = 65_000;
// headersTimeout must be greater than keepAliveTimeout
server.headersTimeout = 66_000;Cloudflare
Cloudflare won't wait forever for your origin either — but when the origin times out, Cloudflare returns its own 524 rather than a 504 (a 504 from Cloudflare usually means your origin or an intermediate proxy generated it). The default origin-response limit is 100 seconds. If a Cloudflare-fronted endpoint legitimately needs longer, the usual answers are moving the work into a background job or streaming early data — not begging the timeout upward.
Returning 504 Correctly (When You Really Are the Gateway)
Most application code should never return 504. The exception: when your code is the middleman — a backend-for-frontend, an API gateway, a service that proxies to another service. Then 504 is exactly right for "my upstream didn't answer in time."
Express / Node.js
app.get("/api/orders", async (req, res) => {
try {
// Give the upstream 10 seconds, not forever
const upstream = await fetch("http://orders-service:4000/orders", {
signal: AbortSignal.timeout(10_000),
});
const body = await upstream.json();
res.status(upstream.status).json(body);
} catch (err) {
if (err instanceof DOMException && err.name === "TimeoutError") {
// We are the gateway, and our upstream timed out: 504 is correct
return res.status(504).json({ error: "upstream_timeout" });
}
return res.status(502).json({ error: "upstream_error" });
}
});Note the split: a timeout maps to 504, while a connection failure or bad response maps to 502 — the same distinction the RFC draws.
Next.js App Router
Same pattern in a route handler — it proxies an upstream API and translates a timeout into 504:
// app/api/orders/route.ts
export async function GET() {
try {
const upstream = await fetch("http://orders-service:4000/orders", {
signal: AbortSignal.timeout(10_000),
cache: "no-store",
});
return Response.json(await upstream.json(), { status: upstream.status });
} catch (err) {
if (err instanceof DOMException && err.name === "TimeoutError") {
return Response.json({ error: "upstream_timeout" }, { status: 504 });
}
return Response.json({ error: "upstream_error" }, { status: 502 });
}
}If you're timing out on your own database rather than a proxied HTTP upstream, this is the wrong code — return 500 or 503 there.
REST API JSON body
Proxies emit bare 504s, but if your gateway code returns one, give machine consumers something to branch on:
HTTP/1.1 504 Gateway Timeout
Content-Type: application/json
{
"error": "upstream_timeout",
"message": "The orders service did not respond within 10 seconds.",
"documentation": "https://api.example.com/docs/errors#upstream_timeout"
}One thing not to include: don't blindly advertise Retry-After or encourage immediate retries. If the upstream is drowning, synchronized retries from every client are how a slowdown becomes an outage. Clients should retry with exponential backoff and jitter — and ideally only for idempotent requests, since a 504 doesn't tell you whether the backend finished the work after the proxy stopped listening.
Making 504s Stop: Align Your Timeouts
Debugging a specific 504 means finding the slow hop (proxy error logs and upstream response-time metrics will name it). Preventing 504s is mostly one design rule:
Timeouts should shrink as you go deeper into the stack. Each layer's timeout should be shorter than the layer above it — client 30s → load balancer 25s → app server 20s → database query 15s. When the innermost call fails first, your application gets to handle the failure and return a clean, informative 503 or 500. When the outermost timer fires first, the proxy slams the door with a generic 504 and your app never even learns the request died — it keeps computing a response nobody will read.
Beyond ordering the timers:
- Move slow work out of the request. Anything that legitimately takes 30+ seconds (exports, imports, report generation) belongs in a background job: return 202 Accepted with a status URL instead of holding the connection open.
- Fix the slowness itself. Missing indexes, N+1 queries, and cold caches cause more 504s than any proxy config. The timeout is the messenger.
- Fail fast when the upstream is known-bad. A circuit breaker that trips after repeated timeouts turns "every request waits 60 seconds then 504s" into "requests fail in milliseconds with 503 while the backend recovers."
- Keep health checks honest. A backend that accepts connections but never responds passes naive TCP health checks while serving nothing but 504s. Health checks should exercise a real code path, with their own short timeout.
504 and SEO
Like the other 5xx codes, an occasional 504 won't hurt you — Google treats it as a temporary problem and retries before taking action. Persistent 504s are another story:
- Indexing — Temporary 504s don't immediately affect indexing. If URLs keep timing out over days, Google starts dropping them from the index.
- Crawl behavior — Crawlers read 504 as a performance problem and back off, reducing crawl rate to avoid making things worse. Persistent timeouts mean less of your site gets crawled.
- The signal to take seriously — Frequent 504s tell Google your site is too slow for Googlebot, which drags on crawl efficiency and, indirectly, rankings. If Search Console shows 5xx spikes, treat it as an infrastructure incident, not an SEO tweak.
Common Pitfalls
- Returning 504 from application code that isn't a gateway. Your handler timing out on a database query is not a gateway timeout — you're not a gateway. Return 500 or 503. Reserve 504 for code that proxies to an upstream server.
- Confusing 504 with 502. Silence past the deadline is 504; a garbage or refused response is 502. Mislabeling sends whoever is on call down the wrong debugging path — "is it slow?" and "is it crashed?" are different investigations.
- Fixing 504s by raising the timeout. Bumping
proxy_read_timeoutfrom 60s to 300s doesn't fix slow work — it makes users stare at a spinner five times longer before the same failure. Raise timeouts only for specific endpoints that legitimately need the time; fix or offload the slow work everywhere else. - Letting the outer timeout fire before the inner one. If the proxy gives up before your app does, the app can't return a useful error and may keep burning resources on a request nobody's waiting for. Order your timers: innermost shortest.
- Retrying non-idempotent requests after a 504. The proxy stopped listening — that doesn't mean the backend stopped working. The order may have been placed, the email sent. Blind retries of POSTs after a 504 are how customers get charged twice; use idempotency keys if you must retry.
- Ignoring keep-alive alignment behind load balancers. An app whose keep-alive timeout is shorter than the load balancer's idle timeout will produce intermittent, maddeningly random 502/504s under load. Keep the app's keep-alive window longer than the balancer's.
Wrapping Up
The rules of thumb:
- 504 means the upstream never answered in time — the proxy's timer expired on silence
- 504 = no response, 502 = bad response, 503 = "not right now," 408 = the client was slow sending the request
- 504 belongs to infrastructure and genuine gateways; app code timing out on its own dependencies should return 500 or 503
- Timeouts shrink inward: each layer shorter than the one above it, so the app fails first with a clean error
- Don't fix 504s by raising timeouts — fix the slow work, or move it to a background job with 202
- After a 504 you don't know whether the work finished — retry with backoff, and only for idempotent requests
For more, see our pages on 504 Gateway Timeout, 502 Bad Gateway, and 503 Service Unavailable. The rest of the "who gave up first?" family is covered in our posts on 502 errors, 503 Service Unavailable, and 499 Client Closed Request — and you can compare 502 vs 503 side by side or explore any pair with the compare tool.