HTTP 504 Gateway Timeout - Causes, Timeouts and Fixes
HTTP 504 Gateway Timeout means a server acting as a gateway or proxy did not receive a response from the upstream server in time. The proxy, load balancer or CDN reached its timeout while your application was still working or stuck. Raising the timeout can hide the symptom, but the lasting fix is to find out why the upstream is slow.
A 504 Gateway Timeout means a server acting as a gateway or proxy did not receive a timely response from the upstream server it needed to complete the request. That is the RFC 9110 definition (section 15.6.5). In practice, nginx, a load balancer or a CDN forwarded the request to your app, waited, hit its timeout and gave up. The proxy is working; the server behind it is slow, stuck or unreachable within the time allowed.
504 versus 502 versus 500
| Code | Who returns it | What happened |
|---|---|---|
| 500 | The app itself | It failed while handling the request |
| 502 | A proxy | The upstream's response was invalid, or the connection was refused or dropped |
| 504 | A proxy | The upstream did not respond before the proxy's timeout |
So with a 504, the question is not "what crashed?" but "what was so slow?"
Who returns a 504
- Reverse proxies like nginx, Apache
mod_proxy, HAProxy and Traefik. - Load balancers such as cloud application load balancers, which have their own idle timeouts.
- CDNs and edge platforms, which time out the connection to your origin. Some use their own codes for this; Cloudflare, for example, returns its non-standard 524 when the origin accepts the connection but sends no response in time, and 522 when the connection cannot be established at all.
- API gateways in front of serverless functions or microservices.
With several layers, the shortest timeout wins. If your CDN gives up after 100 seconds but nginx waits 300, users get the CDN's error first.
Common causes
- Slow database queries – a missing index, a lock held by a long migration, or a table that grew past what a query plan can handle.
- Slow third-party calls – a payment, shipping, email or AI API that is slow or hanging, called synchronously during the request without its own timeout.
- Long-running work in a web request – report generation, large exports, image processing or bulk imports.
- Worker exhaustion – every app worker is busy, so new requests wait in a queue until the proxy gives up.
- Resource starvation – CPU at 100 percent, memory swapping, or disk I/O saturated.
- Network problems between proxy and upstream – a firewall silently dropping packets, so the connection attempt hangs until the connect timeout.
- Deadlocks or infinite loops in application code.
How to diagnose a 504
Measure where the time goes
curl -sS -o /dev/null -w "connect: %{time_connect}s ttfb: %{time_starttransfer}s total: %{time_total}s code: %{http_code}\n" https://example.com/slow-page
A fast connect and a long time-to-first-byte means the server accepted the request and then took a long time to start answering, which is the typical 504 pattern.
Read the proxy's error log
sudo grep "timed out" /var/log/nginx/error.log | tail -n 20
The two lines you are most likely to see:
upstream timed out (110: Connection timed out) while reading response header from upstream– connected fine, but the app took too long to answer. This is governed byproxy_read_timeout(orfastcgi_read_timeoutfor PHP-FPM).upstream timed out (110: Connection timed out) while connecting to upstream– nginx could not even open the connection in time. Look at the network, the firewall, or an app server whose listen backlog is full. This is governed byproxy_connect_timeout.
The log line includes the request URI and the upstream address, so you can see which endpoint and which backend are slow.
Correlate with the app and database
- Application logs or APM traces for the same URL and time window.
- Slow query logs:
slow_query_login MySQL orlog_min_duration_statementin PostgreSQL. - Active queries right now:
SHOW PROCESSLIST;in MySQL, orSELECT pid, now() - query_start AS runtime, query FROM pg_stat_activity WHERE state = 'active' ORDER BY runtime DESC;in PostgreSQL. - Worker saturation: PHP-FPM's
pm.max_childrenwarning, Gunicorn or Puma worker counts versus traffic. - Host resources:
top,vmstat 1,iostat -x 1.
The nginx timeouts that matter
| Directive | Default | What it limits |
|---|---|---|
proxy_connect_timeout |
60s | Establishing the connection to the upstream (usually cannot exceed 75s) |
proxy_send_timeout |
60s | Time between two successive writes of the request to the upstream |
proxy_read_timeout |
60s | Time between two successive reads of the response from the upstream |
fastcgi_read_timeout |
60s | The same as proxy_read_timeout, for FastCGI backends such as PHP-FPM |
Note that the send and read timeouts are measured between operations, not over the whole response. A streaming response that sends a chunk every few seconds will not hit proxy_read_timeout, while a request that is silent for 61 seconds will.
If one endpoint legitimately needs longer, raise the timeout for that location only rather than for the whole site:
location /admin/export {
proxy_pass http://app_backend;
proxy_read_timeout 300s;
}
Remember the layers behind nginx too. PHP has max_execution_time and PHP-FPM has request_terminate_timeout; Gunicorn has --timeout. If the app server kills the request first, the proxy usually logs a closed connection and returns 502 instead of 504.
How to fix it
- Fix the slow query. Run
EXPLAIN(orEXPLAIN ANALYZEin PostgreSQL) on the query and add the missing index. This is often the whole fix. - Put timeouts on outbound calls. Every HTTP client call to a third party should have a connect and read timeout shorter than your proxy's, with a fallback or a clear error.
- Move long work out of the request. Queue the job, return 202 Accepted with a status URL, and notify the user when it is done.
- Add capacity where the queue forms. More workers if CPU and memory allow it, more instances behind the load balancer, or a cache for expensive pages.
- Fix the network path if connections time out: check security groups, firewalls and whether the upstream address is still correct.
- Align timeouts across layers. Make each layer's timeout slightly longer than the one behind it, so the error comes from the layer closest to the problem and is easier to read.
Getting alerted before users complain
504s often start as slowness: pages that took one second now take forty, and only at peak hours. An external HTTP monitor catches both the hard failures and requests that exceed its own timeout. PingRobot's HTTP(S) monitors check every 5 minutes on Free or every minute on Pro, re-check failures from a second region, and alert you by email. Pair it with your own application metrics for response-time trends.
Related guides: 502 Bad Gateway, 503 Service Unavailable and the website down checklist.
Start monitoring free — 10 monitors, no credit card.
Frequently asked questions
What is the default timeout before nginx returns a 504?
proxy_read_timeout defaults to 60 seconds. It applies to the gap between two successive reads from the upstream, not to the whole response, so a response that keeps sending data can run longer.
Should I just increase the timeout?
Only when the long request is legitimate and rare, such as an admin export. For normal page views, a request that takes over a minute is a problem to fix, and longer timeouts tie up workers and let queues grow.
Is a 504 the same as a Cloudflare 524?
They are similar in spirit but different codes. 524 is a non-standard Cloudflare code meaning Cloudflare connected to your origin but did not get an HTTP response before its timeout. 504 is the standard RFC 9110 code any proxy can return.
Why does my monitor show a timeout instead of a 504?
If the whole request takes longer than the monitor's own timeout, the monitor gives up before your proxy does and records a timeout. Either way, the site is too slow for users and should be investigated.