Quick answer: A 504 Gateway Timeout means the edge proxy or load balancer gave up waiting for an upstream service, so the first thing to establish is which hop in the chain actually timed out and what its configured deadline is, rather than assuming your handler crashed. The response is generated by the gateway, not your application, which is why the usual application logs may show nothing at all. The productive debugging path is to reconstruct the request path, compare the gateway's timeout against the upstream's real latency, and then either fix the genuinely slow dependency or change the architecture so a long operation is modeled as an async job instead of a synchronous call. If you treat a 504 as a code bug and add retries at the client without touching the deadline, you do not recover the request; you multiply the load on an upstream that is already too slow.
Developers often reach for the application's error handler first because that is where most failures originate, but a 504 is produced one layer above the application. A reverse proxy, an API gateway, or a load balancer issued the request to the upstream, waited for a response up to a configured limit, and returned 504 when the limit passed. The upstream may still be working on the request when the gateway gives up, which is the second complication: the client sees a failure while the server proceeds and may complete the work, creating the ambiguity that a blind retry then turns into a duplicate operation.
Where the timeout actually lives
A typical request crosses several hops, and each can carry its own deadline, so the first task is to identify the one that fired.
| Hop | Typical symptom | What to check |
|---|---|---|
| Reverse proxy | 504 with proxy error page | proxy read timeout |
| Load balancer | 504 under load | idle/response timeout |
| Upstream service | Slow query or call | latency trace |
| Downstream dependency | Blocked on a third party | nested call timing |
The nested dependency row is the root cause in a surprising number of cases: your service is fast, but it waits on a database query or a third-party call, and the gateway attributes the wait to your endpoint. The fix belongs at the real bottleneck, not at the layer that reported it.
Reconstruct the path and the deadline
Map the request through each proxy and collect the configured timeouts, then reproduce the call and measure where the time actually goes. The comparison between the deadline and the measured latency pinpoints the hop that needs to change.
Calling the upstream directly is the key move because it separates gateway behavior from application behavior. If the direct call is slow, the gateway is honestly reporting a bottleneck; if it is fast, the timeout configuration or a connection between the two is the problem.
Fix the bottleneck, then reconsider the interaction model
For a genuinely slow but bounded operation, optimize the query or the dependency and align the gateway timeout with a realistic upper bound. For an operation that is inherently long, do not keep stretching a synchronous timeout; return an accepted job and let the client poll or subscribe for completion.
Because the gateway can abandon a request while the server continues, any retry path also needs an idempotency key so a repeated attempt after a 504 does not create duplicate work. Retrying without that protection is the classic way a timeout incident becomes a data duplication incident.
Use the contract to keep deadlines explicit
Documenting the expected response time and the async job shape in the API contract lets generated clients handle 202 and polling correctly instead of assuming every endpoint answers synchronously. The contract is also where the timeout-driven interaction model is declared, so the edge configuration, the client behavior, and the documented behavior agree rather than each being tuned independently.
The practical rule is to treat a 504 as a deadline problem on a specific hop, not as a generic crash. Identify the hop, measure the upstream latency directly, fix the real bottleneck or move long work into an async job, and make any retry idempotent. A client that simply retries a timed-out call is not being resilient; it is reissuing work the server may already be doing, against an upstream that could not answer in time.
Frequently asked questions
Why are there no application errors when I get a 504?
The 504 is generated by the gateway after it stops waiting, not by your application. Your handler may log nothing because it never saw the deadline, and it may still finish the request afterward.
Should I increase the gateway timeout to fix this?
Only if the operation is bounded and genuinely needs the extra time. For inherently long work, stretching a synchronous timeout just hides the problem; an async job with polling is the correct model.
Is it safe to retry after a 504?
Only with an idempotency key, because the upstream may have completed the work after the gateway gave up. Without it, a retry can create a duplicate operation.
How do I tell if the gateway or the upstream is at fault?
Call the upstream directly and measure its timing. If it is slow, the gateway is reporting a real bottleneck; if it is fast, the gateway timeout or the connection to the upstream is the problem.
What does a good long-operation response look like?
A 202 Accepted with a job id, followed by a status endpoint the client polls or a subscription that resolves when the result is ready, keeping the original request short.
This is part of the The API Debugging Guide — start there for the full picture of diagnosing API failures.
Top comments (0)