Nginx 504 Gateway Timeout: Complete Fix Guide
A 504 Gateway Timeout means Nginx successfully forwarded a request to its upstream server (PHP-FPM, a Node.js app, a Python backend, or another proxy), but the upstream did not produce a complete response before the configured timeout elapsed. Unlike a 502 Bad Gateway, which means the upstream returned an invalid or no response at all, a 504 specifically signals that the upstream accepted the connection and was working — just too slowly.
The boundary between 502 and 504 matters for diagnosis. A 502 means the connection to the upstream failed outright (process down, wrong port, refused). A 504 means the connection succeeded but the upstream hung. That is why fixing a 504 is rarely about restarting things and almost always about understanding why the backend is slow: a heavy database query, a third-party API call that stalls, a PHP memory limit that triggers a fatal error mid-request, or simply a timeout value set too low for legitimate long-running operations like file uploads or report generation.
Step 1: Identify Which Request Is Timing Out
Before changing any timeout values, find out exactly which request triggers the 504. Open the Nginx error log and look for upstream timed out messages:
tail -100 /var/log/nginx/error.log | grep "timed out"
A typical entry looks like this:
2026/07/16 10:14:22 [error] 1832#1832: *7 upstream timed out
(110: Connection timed out) while reading response header from upstream,
client: 203.0.113.5, server: example.com,
request: "POST /api/generate-report HTTP/1.1",
upstream: "fastcgi://unix:/run/php/php8.2-fpm.sock"
The request and upstream fields tell you which endpoint and which backend are involved. If every request 504s, the upstream is broadly broken. If only specific endpoints (exports, sync, uploads) fail, you are dealing with legitimately slow operations that need the fixes below.
Step 2: Increase Proxy Timeout Settings
If the upstream is legitimately slow but functional, the quickest fix is to extend Nginx's patience. The relevant directives depend on which proxy module you use. For a reverse proxy (proxy_pass):
location / {
proxy_pass http://backend;
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
For FastCGI (PHP-FPM, fastcgi_pass):
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_read_timeout 300s;
fastcgi_send_timeout 300s;
}
proxy_read_timeout controls how long Nginx waits between two successive reads from the upstream. The default is 60 seconds, which is too short for report generation or large imports. Raising it to 300s gives slow scripts room to finish. After editing, validate and reload:
nginx -t && nginx -s reload
Step 3: Check Upstream Application Performance
Increasing the timeout only hides the symptom. The real fix is to find out why the backend is slow. Common culprits: slow database queries, synchronous third-party API calls, and PHP memory exhaustion. Check for slow database queries by enabling MySQL's slow query log:
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;
Then review /var/log/mysql/slow.log for queries exceeding two seconds. A single missing index on a large table can turn a 50ms query into a 60-second one. For third-party API calls, add a client-side timeout so your app fails fast instead of hanging until Nginx gives up:
// PHP example with cURL
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
For PHP memory limits, check the error log. A request that hits memory_limit raises a fatal error and the response never completes, which Nginx reports as a 504:
# Raise the memory limit in php.ini
memory_limit = 256M
Step 4: Configure Nginx Upstream Keepalive
Every time Nginx opens a new TCP connection to the upstream, it pays a latency cost. For high-traffic sites, that overhead alone can push responses past the timeout. Enabling keepalive connections to the upstream reuses TCP connections and removes this overhead:
upstream backend {
server 127.0.0.1:8080;
keepalive 32;
}
server {
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_read_timeout 300s;
}
}
The keepalive 32 directive maintains a pool of 32 idle connections. proxy_http_version 1.1 and proxy_set_header Connection "" are required — without them Nginx sends Connection: close and the pool never reuses connections. After enabling keepalive, watch your upstream error log: the frequency of "upstream timed out" entries should drop noticeably under load, because each request no longer pays the TCP and TLS handshake cost.
One caveat: keepalive only helps when the upstream itself can hold idle connections open. If your backend is a serverless function or a short-lived PHP process that exits after each request, the pool will be re-established constantly and you will see no benefit. In that case, focus on Step 3 (application performance) and Step 5 (caching) instead.
Step 5: Implement Caching to Reduce Response Time
Caching is the most effective long-term defense against 504s. If a slow endpoint's output can be cached, serve the cached version instead of hitting the upstream every time. For proxy backends:
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m
max_size=100m inactive=60m use_temp_path=off;
server {
location /api/ {
proxy_cache api_cache;
proxy_cache_valid 200 10m;
proxy_cache_use_stale error timeout updating;
proxy_pass http://backend;
proxy_read_timeout 300s;
}
}
For FastCGI backends, use fastcgi_cache with the same pattern. The proxy_cache_use_stale timeout updating directive is especially valuable: when the upstream times out, Nginx serves the stale cached copy instead of returning a 504, keeping the site available while the backend recovers.
Quick Reference: Causes and Fixes
| Symptom / Log Message | Root Cause | Fix |
|---|---|---|
upstream timed out (110) on POST /export |
Long-running script exceeds 60s default | Raise proxy_read_timeout / fastcgi_read_timeout |
504 only on /api/* endpoints |
Slow upstream app or external API | Add client-side timeouts; optimize queries |
| 504 + PHP fatal error in log | memory_limit hit mid-request |
Increase memory_limit; profile script |
| 504 under high concurrency | New upstream connection overhead | Enable upstream keepalive pool |
| 504 on cacheable GET requests | No caching; every request hits backend | Configure proxy_cache / fastcgi_cache |
| 504 + slow_query_log entries | Missing DB index on large table | Add index; rewrite query |