How to Fix "Nginx 504 Gateway Time-out" on Long Running Requests
Quick answer
A request that takes a while to process β a report generation, a large data export, a slow third-party API call β fails with a 504 instead of eventually...
A request that takes a while to process β a report generation, a large data export, a slow third-party API call β fails with a 504 instead of eventually completing. Nginx gave up waiting for your backend to respond within its configured timeout window, which is deliberately conservative by default.
The Problem
The request hangs for a while and then fails, rather than either completing or failing instantly:
$ curl -i https://yourapp.com/reports/generate
HTTP/1.1 504 Gateway Time-out
The Nginx error log confirms it timed out waiting rather than getting an actual error response from the backend:
2026/08/07 16:05:44 [error] 8821#8821: *312 upstream timed out (110: Connection timed out)
while reading response header from upstream, client: 203.0.113.5,
server: yourapp.com, request: "GET /reports/generate HTTP/1.1",
upstream: "http://127.0.0.1:3000/reports/generate"
Why It Happens
Nginx enforces several separate timeout thresholds for proxied requests, and the default values (often 60 seconds) are tuned for typical fast API responses, not long-running operations. Once the relevant timeout is exceeded, Nginx severs the connection and returns 504 regardless of whether the backend was actually still working productively. This happens when:
- A legitimately slow operation β a large report, a heavy database aggregation, a synchronous call to a slow external API β genuinely needs more time than the default timeout allows.
- The backend itself is stuck or degraded (a slow query, lock contention, an exhausted connection pool) and would eventually time out on its own, but Nginx gives up first.
- A specific timeout directive was overlooked β there are several distinct ones (
proxy_connect_timeout,proxy_send_timeout,proxy_read_timeout), and raising only one while the request is actually blocked on a different phase doesn't help.
The Fix
Identify which phase is actually timing out from the specific wording in the error log β "reading response header" (shown above) points at proxy_read_timeout, which is the most common culprit for a slow backend response:
location /reports/ {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 300s;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
}
Scope the higher timeout to just the specific slow endpoint rather than raising it globally, so fast endpoints still fail quickly if something actually goes wrong with them:
location = /reports/generate {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 300s;
}
location / {
proxy_pass http://127.0.0.1:3000;
# default shorter timeout for everything else
}
Reload Nginx after any config change:
sudo nginx -t
sudo systemctl reload nginx
If you're using PHP-FPM as the backend, its own execution time limit also needs to be raised to match, or PHP will kill the script before Nginx's longer timeout even matters:
; php.ini
max_execution_time = 300
For Node.js, check whether the HTTP server itself has a shorter default timeout that needs adjusting independently of Nginx:
server.timeout = 300000; // milliseconds
Still Not Working?
Raising timeouts is a reasonable short-term fix, but a request that legitimately takes several minutes is often a sign the operation should be moved out of the synchronous request-response cycle entirely. Consider converting long-running operations into a background job with a status-polling endpoint instead of holding a connection open the whole time β this avoids tying up an Nginx worker and a backend process for minutes at a time, and gives users better feedback than a spinning loading indicator:
POST /reports/generate -> returns { "job_id": "abc123" } immediately
GET /reports/status/abc123 -> { "status": "processing" } or { "status": "done", "url": "..." }
This pattern scales far better under concurrent load than long-lived synchronous requests, since it frees up server resources immediately instead of holding a worker thread or process hostage for the duration of a slow operation, and it sidesteps timeout tuning entirely for anything that genuinely takes minutes rather than seconds.