Guides

8 min readUpdated

504 Gateway Timeout: causes and how to fix it

Quick answer

A 504 Gateway Timeout means a gateway or proxy, such as a CDN, a load balancer or nginx, waited for the server behind it and gave up when its timer ran out. The upstream may still be working on the request. Usually something is slow rather than broken: a long database query, a saturated worker pool, a job that belongs in the background, or an upstream no longer accepting connections. Find the slow layer rather than raising the timeout.

01What does 504 Gateway Timeout mean?

RFC 9110 defines a 504 as the response of a server that, "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". Something was waiting, a timer expired, and the waiting server answered on the upstream's behalf. Its neighbours in the 5xx range are listed in HTTP status codes.

That makes a 504 different from a 502 Bad Gateway. A 502 is a bad or missing answer and usually arrives at once. A 504 arrives after a delay equal to some configured timeout, and the upstream may still be processing the request when the visitor sees the error. A payment, an import or an email batch can complete after the 504 was shown, which is why endpoints that users retry after a timeout need to be idempotent.

02Which timeout fired? Mapping the timeout chain

Every hop in front of the application runs its own timers, and the first one to expire decides who returns the 504. The defaults below are the documented ones; your configuration may differ.

Two details cause most confusion. nginx's read timeout applies between two successive reads, not to the whole response, so a streaming response that sends data every 30 seconds never trips a 60-second limit while a page that computes silently for 61 seconds always does. And on Linux, in the usual non-thread-safe builds, PHP's 30-second max_execution_time does not count time spent waiting on system calls, streams or database queries, so a script blocked on a slow query can run past nginx's 60 seconds and the visitor gets a 504 although PHP's own limit looks shorter.

LayerSettingDocumented defaultWhat it limits
Cloudflare (CDN example)Proxy read timeout125 seconds, then error 524Time for the origin to start responding
AWS Application Load BalancerConnection timeout / idle timeout10 seconds / 60 secondsConnecting to the target / inactivity on the connection
nginxproxy_connect_timeout60 seconds, usually capped at 75Establishing the TCP connection upstream
nginxproxy_read_timeout, fastcgi_read_timeout60 secondsThe gap between two successive reads
nginxproxy_send_timeout60 secondsThe gap between two successive writes upstream
Gunicorntimeout30 secondsA silent worker is killed and restarted
PHPmax_execution_time30 secondsScript time; in usual Linux builds excludes waiting on I/O and databases
MySQLlong_query_time10 secondsOnly the slow query log threshold, not a timeout

03What the logs say when the timeout fires

nginx records which phase timed out, and the two phases point to different causes. Both lines below were reproduced on nginx 1.22.1, one against a backend that sleeps before answering and one against a backend whose listen queue was full.

"While reading response header" means the application accepted the request but is slow: a query, a lock, an external API call, or a worker pool so busy that requests queue inside it. "While connecting" means the TCP connection never completed: a firewall silently dropping packets, a host that is down, or an application that has stopped accepting connections so its listen queue is full.

PHP-FPM logs its side too: "server reached pm.max_children setting" means every worker was busy and new requests waited, and "executing too slow" means a request passed request_slowlog_timeout, with its stack trace written to the slowlog file.

nginx error log: two different 504s
# the upstream accepted the request but sent no response headers in time
upstream timed out (110: Connection timed out) while reading response header from upstream

# the TCP connection to the upstream was never established
upstream timed out (110: Connection timed out) while connecting to upstream

04How to find the slow part

Time the same request at each hop with curl's timing variables. The gap between connect and first byte is time spent inside the upstream; a slow connect points at the network or a full listen queue.

If the first-byte time is just as long when you hit the application directly, the proxies are innocent and the application needs profiling. Turn on the database slow query log (MySQL's long_query_time defaults to 10 seconds, too high for web requests) and PHP-FPM's request_slowlog_timeout, which writes a stack trace for any request that runs longer than the value you set.

Timing a request at each hop (curl 7.88, iproute2 6.1)
# from outside, through the CDN
curl -s -o /dev/null -w 'connect %{time_connect}s  tls %{time_appconnect}s  first byte %{time_starttransfer}s  total %{time_total}s\n' https://example.com/slow-page

# directly at the application, skipping every proxy
curl -s -o /dev/null -w 'connect %{time_connect}s  first byte %{time_starttransfer}s\n' -H 'Host: example.com' http://127.0.0.1:8000/slow-page

# is the application accepting connections? On a LISTEN socket, Recv-Q above Send-Q means a full queue
ss -ltn 'sport = :8000'
nstat -az TcpExtListenOverflows TcpExtListenDrops

05How to fix a 504 properly

Raising a timeout is sometimes right, for one endpoint that is legitimately slow. As a global fix it only makes visitors wait longer for the same failure and lets slow requests pile up and hold workers.

  • Fix slow queries first: add the missing index, remove the N+1 loop, or cache the result
  • Move long jobs off the request: accept the export or import, return 202 Accepted with a job ID, and let the browser poll for the result
  • Give the worker pool real capacity: if PHP-FPM logs that it reached pm.max_children, add workers that fit in memory or make each request faster
  • Set a longer timeout only on the endpoint that needs it, never server-wide
  • Stream long responses, so data keeps flowing within the read timeout
  • Keep each layer's timeout slightly shorter than the layer in front of it
Per-endpoint timeouts instead of one global value (checked with nginx -t)
location / {
    proxy_pass http://app;
    proxy_connect_timeout 5s;    # TCP connect to the upstream
    proxy_send_timeout    30s;   # gap between two writes to the upstream
    proxy_read_timeout    30s;   # gap between two reads from the upstream
}

# one known slow endpoint gets a longer budget, not the whole site
location /reports/export {
    proxy_pass http://app;
    proxy_read_timeout 300s;
}

location ~ \.php$ {
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    fastcgi_read_timeout 60s;
}

06Keepalive and idle timeouts that do not line up

A connection between two layers has an idle timer at each end. If the layer in front keeps a connection open longer than the layer behind it, it will eventually send a request on a connection the other side is closing, which usually shows up as a 502. The mirror case produces 504s: a balancer whose idle timeout is shorter than the time the application needs before its first byte will cut off every slow request, however healthy the application is.

AWS states both halves for its Application Load Balancer: the idle timeout defaults to 60 seconds, a target that does not respond within it produces a 504, and the application's own keep-alive timeout should be longer than the balancer's. The same rule holds for any chain of proxies.

07504s behind a CDN, load balancer or WAF

Behind an edge provider, the provider's timeout is usually the outermost one. Cloudflare, as one documented example, returns its own error 524 when the origin accepts the connection but sends no response within its proxy read timeout, 125 seconds by default according to its documentation, and recommends status polling for long processes. Other providers have equivalent limits under their own codes.

A connect-phase timeout from the edge often means the origin firewall drops the edge's packets instead of rejecting them. Dropping makes the edge wait out its connect timeout; rejecting would have produced an instant refused connection and a 502. Either way, allow the provider's ranges, as described in how to hide and protect the origin.

Under attack, rising 504s are a common early symptom. An HTTP flood or a Slowloris attack ties up workers and connection slots until legitimate requests queue past every timeout. That is a capacity problem at the origin, and the durable fix is to filter the traffic before it arrives, together with sensible rate limiting on expensive endpoints.

08504 troubleshooting matrix

What you observeMost likely causeCheckFix
504 after exactly 60 secondsnginx's default proxy_read_timeout or fastcgi_read_timeoutError log: "while reading response header"Find the slow code path; raise the timeout only for that location
504 at the CDN, origin logs a 200 laterRequest longer than the edge allowsRequest duration in the origin's access logMake the job asynchronous
504 on every request after the connect timeoutFirewall dropping packets, host down, or listen queue fullError log: "while connecting"; ss -ltn Recv-QAllow the proxy's ranges; restore the service; add capacity
504 only at peak timesWorker pool exhaustedPHP-FPM "reached pm.max_children"; application metricsFaster requests, or more workers that fit in memory
504 on one endpoint onlySlow query or slow external APISlow query log, PHP-FPM slowlog, tracingIndex, cache, or move the work off the request
504 during a surge from many sourcesApplication-layer flood exhausting workersRequest rate per path and clientFilter the flood upstream

Questions

Frequently asked questions

No. Both are generated by a proxy or gateway, but a 502 means the upstream returned something invalid or nothing at all, often instantly, while a 504 means the upstream did not respond before the proxy's timer expired. A 504 therefore always arrives after a delay. Different log lines and different fixes follow from that, so check which code you actually received first.

It removes the error for requests that finish just after the old limit, which is right for a single slow endpoint such as an export. As a global change it makes visitors wait longer, keeps more workers busy and hides the real cause, usually a slow query or a job that belongs in a background queue. Raise it per location, and fix the slow work itself.

Sixty seconds is the default for nginx's proxy_read_timeout, proxy_send_timeout and fastcgi_read_timeout, and for the idle timeout of AWS Application Load Balancers. An exact 60-second delay strongly suggests one of those defaults fired. Check which layer's error log has a timeout entry at that second, and remember that the layer that timed out is rarely the slow one.

Possibly. A 504 only means the proxy stopped waiting. The upstream may have finished the work afterwards, so an order, payment or form submission can succeed even though you saw an error. Check your account or email before retrying. Developers should make such endpoints idempotent, for example with an idempotency key, so a retry after a timeout cannot charge or create anything twice.

Yes. An application-layer flood or a slow-connection attack occupies workers and connection slots, so legitimate requests wait in queues until a proxy timeout expires. The visible symptom is a sudden rise in 504s while the worker pool is saturated. Raising timeouts makes this worse, because each stuck request holds a slot for longer; filtering the hostile traffic before it reaches the origin addresses the cause.

Sources

Sources and verification

Timeout snippet checked with nginx -t on nginx 1.22.1; both 504 log lines reproduced against local test backends (a sleeping backend and a full listen queue, which ss showed as Recv-Q 2 against Send-Q 1); curl 7.88.1 timing format, ss and nstat (iproute2 6.1) run on Debian 12, October 2026.

  1. 01RFC 9110, HTTP Semantics, section 15.6.5: 504 Gateway Timeout · RFC Editor
  2. 02Module ngx_http_proxy_module (proxy_connect/read/send_timeout) · nginx.org
  3. 03Module ngx_http_fastcgi_module (fastcgi_read_timeout) · nginx.org
  4. 04Troubleshoot your Application Load Balancers: HTTP 504 · AWS
  5. 05Edit attributes for your Application Load Balancer: connection idle timeout · AWS
  6. 06Error 524: a timeout occurred · Cloudflare docs
  7. 07PHP runtime configuration: max_execution_time · php.net
  8. 08PHP-FPM configuration directives · php.net
  9. 09PHP-FPM source: pm.max_children warning · php-src on GitHub
  10. 10Gunicorn settings: timeout · Gunicorn
  11. 11The slow query log · MySQL 8.4 Reference Manual

Last updated