Blog
A 504 means a proxy stopped waiting for a slow upstream. Learn how it differs from a 502, which proxy timeouts apply and how to find and monitor the slow layer.
The most common 504 gateway timeout causes are an upstream that answers too slowly, such as a slow database query, exhausted workers or a hanging third-party API call, and a proxy timeout that is shorter than the work the request needs. A 504 is returned by a proxy, load balancer or CDN that forwarded the request and gave up waiting for the response. To fix it, you find the layer that is slow, make inner layers fail before outer ones, and watch response time so you see the slowdown before it turns into errors.
A 504 Gateway Timeout is never generated by your application itself. It comes from something in front of it: nginx, HAProxy, a cloud load balancer, an API gateway or a CDN. That component accepted the client request, passed it upstream and did not receive a response within its configured timeout.
The important detail is that the upstream usually still exists and is often still working on the request. The proxy simply stopped waiting. The query may finish ten seconds later, write to the database and return a response that nobody reads. That is why 504s can leave behind duplicate orders or half-finished jobs when users retry.
Both codes come from a proxy, but they describe different failures. A 502 means the proxy got an invalid response or none at all: the connection was refused, reset or closed early. A 504 means the connection worked, but the answer did not arrive in time. If you need the full list of 502 failure modes, the guide to 502 Bad Gateway causes and how to debug each one covers them.
The two codes often appear together because they depend on which timeout fires first. Gunicorn kills a worker after 30 seconds by default. If nginx waits 60 seconds, the worker dies first, the connection closes and the client sees a 502. If you raise the Gunicorn timeout to 120 seconds, the same slow request now produces a 504 at 60 seconds. Same root cause, different code.
A missing index, a query that scans a growing table or a row lock held by a long transaction keeps the request waiting. These problems tend to grow with data volume: an endpoint that took 200 ms at launch can take 40 seconds a year later without a single code change.
When all PHP-FPM children, Gunicorn workers or Node event loop capacity are busy, new requests wait in a queue. Each individual request may be fast, but the time spent waiting for a free worker pushes the total past the proxy timeout. The same happens when the application waits for a free database connection from a pool that is too small or leaking connections.
Your application calls a payment API, a mail service or an internal microservice. If that HTTP client has no timeout, one slow dependency stalls the whole request. Many HTTP libraries default to no timeout or a very long one, so this cause is easy to introduce and hard to spot.
CSV exports, report generation, large uploads, image processing and bulk imports are legitimately slow. They work in development with small data and start timing out in production.
A firewall rule that silently drops packets, a security group change, or an upstream address that no longer routes produces connect timeouts instead of refusals. The proxy waits for the TCP handshake until proxy_connect_timeout expires and then returns a 504.
Every hop in the chain has its own timeout, and the shortest one decides when the client sees an error.
nginx uses three proxy timeouts, each defaulting to 60 seconds: proxy_connect_timeout, proxy_send_timeout and proxy_read_timeout. For PHP-FPM the equivalents are fastcgi_connect_timeout, fastcgi_send_timeout and fastcgi_read_timeout. Note that proxy_read_timeout applies between two successive reads, not to the whole response. A streaming response that sends data regularly can run longer than 60 seconds.
upstream app {
server 127.0.0.1:8000;
}
server {
location / {
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
proxy_pass http://app;
}
# Raise only where slow work is expected
location /exports/ {
proxy_read_timeout 120s;
proxy_pass http://app;
}
}A short connect timeout is useful: a healthy upstream on the same network connects in milliseconds, so waiting 60 seconds for a handshake only delays the error.
Managed load balancers have an idle or request timeout, often 60 seconds by default, that you configure on the load balancer or its target settings. If your nginx behind it allows 120 seconds, the load balancer still cuts the request at 60. Check this value whenever you raise a timeout further inside.
When Cloudflare proxies your site, an origin that does not respond in time usually produces a 524, not a 504. A 522 means Cloudflare could not connect to the origin at all. A 504 seen through Cloudflare typically comes from your own origin proxy or load balancer and was passed through. That distinction alone tells you which side to investigate.
A 504 is a symptom of a slow layer, not a timeout that is too short. Find that layer, make inner timeouts shorter than outer ones, and move slow work out of the request path.
Raising proxy_read_timeout to 300 seconds keeps workers busy longer, which makes pool exhaustion worse under load. Instead, order the timeouts so each inner layer gives up before the one outside it. Then the application can return a clear error, log it and release resources, instead of the proxy cutting the connection blindly.
CDN / edge 100 s
Load balancer idle 60 s
nginx proxy_read_timeout 30 s
App request timeout 25 s
DB statement timeout 10 s
Outbound HTTP client 5 sDatabase-level limits are cheap to add. In PostgreSQL you can set a per-role limit:
ALTER ROLE app SET statement_timeout = '10s';For genuinely long work, change the pattern: accept the request, enqueue a background job, return 202 Accepted with a job ID, and let the client poll or receive a notification. Exports and imports almost always belong in a queue.
Work from the outside in and remove one layer at a time.
curl -o /dev/null -s -w 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total} code=%{http_code}\n' https://example.com/slow-endpointIf connect and TLS are fast but TTFB is huge, the server side is slow. If connect hangs, look at the network path.curl --resolve example.com:443:203.0.113.10 https://example.com/slow-endpoint. If it is fast here, the problem is at the edge or between edge and origin.curl http://127.0.0.1:8000/slow-endpoint. Still slow means the application or something behind it is the problem.log_format timing '$remote_addr "$request" $status '
'rt=$request_time uct=$upstream_connect_time '
'uht=$upstream_header_time urt=$upstream_response_time';
access_log /var/log/nginx/timing.log timing;High uct points to connection or queueing problems, high uht to slow processing, and urt close to your read timeout to requests that hit the limit.504s rarely appear out of nowhere. Response time on the affected endpoint climbs for days or weeks first, as tables grow or traffic increases. A monitor that only checks whether the status code is 200 stays green until the moment requests cross the timeout.
m0nitor is one option for this: it checks websites, APIs and servers on a fixed interval from distributed probes in several regions, and alert workflows can notify a channel, create an incident or call a webhook when a monitor fails. Any monitoring setup that records response times and alerts on them covers the same ground.
It is a server-side problem. The proxy, load balancer or CDN in front of the application timed out waiting for the upstream. Reloading the page or clearing the browser cache does not fix the cause, although a retry may succeed if the slowdown was brief.
First check $upstream_response_time in the logs to see whether requests hit proxy_read_timeout or fastcgi_read_timeout. Then fix the slow part, such as a query, worker count or external call. Raise the timeout only for specific locations where slow work is expected, and make sure the load balancer in front allows at least as much time.
Cloudflare uses 524 when it connected to your origin but did not receive a response in time. A 504 shown through Cloudflare usually comes from your origin's own proxy or load balancer. Both mean the origin is too slow, but they point to different timeouts in the chain.
Yes. If your application waits on an external API without a short client timeout, that wait counts toward the proxy timeout. Set explicit timeouts on every outbound call, typically a few seconds, and handle the failure in your code instead of letting the proxy return a 504.