nginx has five timeouts and four are the wrong one
Somebody adds proxy_read_timeout 300s; to the configuration, reloads, and the 504 keeps arriving at exactly sixty seconds. The directive is correct, the syntax is fine, nginx accepted it, and it applies to nothing.
nginx has a separate family of timeout directives for each way of passing a request upstream, and they do not substitute for one another.
| If the location uses | The directives are |
|---|---|
proxy_pass | proxy_connect_timeout, proxy_send_timeout, proxy_read_timeout |
fastcgi_pass | fastcgi_connect_timeout, fastcgi_send_timeout, fastcgi_read_timeout |
uwsgi_pass | The uwsgi_ equivalents |
grpc_pass | The grpc_ equivalents |
A PHP site talks to PHP-FPM through fastcgi_pass. So on the overwhelming majority of WordPress and Laravel installations, the directive people reach for first is the one that has no effect.
Find out which one applies before editing anything:
sudo nginx -T 2>/dev/null | grep -nE '(proxy|fastcgi|uwsgi|grpc)_pass|_read_timeout'
nginx -T prints the whole configuration as nginx assembled it, includes resolved. That matters, because the directive that is actually in force is frequently in a snippet file three includes deep that nobody remembers.
Read, send, connect: three different waits
All three default to sixty seconds, and each measures something different.
Connect is how long to wait for the upstream to accept the connection. Exceeding it usually means the process pool is full or the socket is gone, and it produces a 502 more often than a 504.
Send is how long to wait while transmitting the request. Relevant for large uploads, irrelevant for most page loads.
Read is the one that produces the classic 504, and its definition has a subtlety worth knowing. It is not the total time allowed for the response. It is the maximum interval between two successive reads from the upstream.
Which means an upstream that dribbles out one byte every thirty seconds will never trigger it, and can hold a worker open indefinitely. And it means the sixty-second limit is not a budget for the whole page, it is a rule about silence. A backend that thinks for fifty seconds, sends a header, then thinks for another fifty, has not exceeded a sixty-second read timeout at any point.
The chain below nginx
Three limits sit in series on a PHP site, and the order between them decides whether you get a useful error or a useless one.
| Layer | Setting | What happens when it fires |
|---|---|---|
| PHP | max_execution_time | A fatal error, logged, with a stack location |
| PHP-FPM | request_terminate_timeout | The worker is killed. nginx sees a closed connection and returns 502. |
| nginx | fastcgi_read_timeout | nginx stops waiting and returns 504. The backend carries on working. |
Set them so the innermost gives up first: PHP's limit below FPM's, and FPM's below nginx's. Then a runaway request produces a PHP fatal error naming the file and the line, which is a diagnosis.
Get the order wrong and nginx gives up first. The visitor receives a 504, and the backend keeps running the request to completion, holding a worker and a database connection, with nobody left to receive the result. Under load that is how a slow page turns into an outage: the workers are all busy producing responses for connections that were abandoned minutes ago.
That distinction is also the quickest way to read which layer fired. A 502 at the moment of failure means something killed the process. A 504 means something stopped waiting for it, and the process is probably still going: the relay is reporting somebody else's behaviour.
Making the change in the right place
Per location, not globally. A global increase gives every endpoint on the machine permission to hold a connection for five minutes, which is a resource decision affecting the whole server for the sake of one slow admin page.
location ~ ^/wp-admin/(export|import)\.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_read_timeout 300s;
}
Then check the syntax and reload rather than restart, so that in-flight requests are not dropped:
sudo nginx -t && sudo systemctl reload nginx
And add a comment saying why the value is what it is. A timeout of 300 with no explanation is a number the next person will either be afraid to touch or will remove without knowing what depended on it.
Buffering, which produces a different failure at the same moment
Worth mentioning because it appears alongside timeouts and gets blamed for them.
By default nginx buffers the upstream response, spilling to disk when it exceeds the configured buffer sizes. A large response therefore involves temporary files, and a full disk or a permissions problem in the temporary directory produces an error at roughly the point where a slow large response would have timed out. The error log will say so explicitly, which is the reason to read the error log before changing timeout values.
sudo tail -50 /var/log/nginx/error.log
The entry for a genuine read timeout says upstream timed out, and it names the upstream and the request. If the log says something else, the timeout was not the problem.
Raising it is the last option, not the first
A longer timeout does not make anything faster. It changes what the visitor experiences from an error at sixty seconds to a blank page at three hundred, and it lets more slow requests accumulate at once, which is how one slow endpoint takes a site down.
Where the slowness is genuinely long work, the answer is to move it out of the request. Where it is a slow dependency, the answer is a timeout on that dependency rather than a longer one on your own server. And where you cannot change any of the values because the configuration belongs to somebody else, which is most shared hosting, the constraint forces the better fix anyway: make the request finish sooner, because nothing else is available.
Defined in RFC 9110, section 15.6.5, which puts it in one sentence: “A server 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.” Read on 3 August 2026.