Nginx "504 Gateway Time-out": find out what's slow before raising the timeout
Fix Nginx "504 Gateway Time-out": read the error log, measure the backend, set proxy_read_timeout or fastcgi_read_timeout, and check Cloudflare and load balancers.
by OhSudo ··
Checked on real software: Nginx · November 2026How we check
Not sure this is your cause?Find it step by step: Website down →
Part of a learning path:Web servers: nginx and HTTPS in production
504 Gateway Time-outnginx/1.24.0Nginx passed the request on to your application, waited, and gave up. By default that means 60 seconds passed without an answer. Nginx itself is healthy, and it’s telling you that whatever sits behind it is slow, stuck or unreachable.
Raising the timeout does stop the error, but it may also only hide the problem: visitors then wait longer for the same failure. Find out what’s slow first.
- Browserrequest
- nginxwaits for the answer(works)proxy_pass, then waits(fails)
- Your appno answer within the timeout(breaks here)
TL;DR
sudo tail -n 20 /var/log/nginx/error.log | grep -i 'timed out'curl -o /dev/null -s -w 'status %{http_code}, %{time_total}s\n' http://127.0.0.1:3000/slow-pageIf the backend is really slow and it’s meant to be, raise the limit for that one path:
location /reports/ { proxy_pass http://127.0.0.1:3000; proxy_read_timeout 300s;}1. Read the error log
sudo grep 'timed out' /var/log/nginx/error.log | tail -n 5upstream timed out (110: Connection timed out) while reading response header from upstream, client: 203.0.113.7, server: example.com, request: "GET /report HTTP/1.1", upstream: "http://127.0.0.1:3000/report"The words after while say which limit expired:
| The log says | Waiting for | Setting |
|---|---|---|
while connecting to upstream |
A connection to the backend | proxy_connect_timeout |
while reading response header from upstream |
The first byte of the answer. The usual one | proxy_read_timeout |
while sending request to upstream |
The backend to accept the request | proxy_send_timeout |
No such line in Nginx’s log at all? Then this Nginx didn’t produce the 504. Look at what’s in front of it (section 5).
2. Connecting: the backend can’t be reached
A timeout while connecting means the packets get no answer. The application might be on another machine, with a firewall dropping them, or the address might be wrong. Test from the Nginx server:
nc -vz -w 5 10.0.0.20 3000A closed port refuses at once and gives a 502 Bad Gateway instead, so a timeout points to a firewall or a dead address. “Connection timed out” vs “Connection refused” explains the difference. Don’t wait 60 seconds for this one. A short limit, such as proxy_connect_timeout 5s;, is usually better.
3. Reading: measure the backend
Go around Nginx, on the Nginx server, and time the same request:
curl -o /dev/null -s -w 'status %{http_code}, first byte %{time_starttransfer}s, total %{time_total}s\n' \ http://127.0.0.1:3000/reportA slow answer here means the problem is in the application, and that’s where to look:
- The database. A slow query is the top cause. Look at the slow query log or
pg_stat_activity. A lock, or a pool with no free connections, makes requests wait too, as in too many clients. - An outside API with no timeout. One slow service makes every request hang.
- All the workers are busy. Fast pages time out too, because they’re queuing behind slow ones. Under load, this looks like the whole site being down.
- Not enough resources. Check
top,free -manddf -h. Swapping makes everything crawl, and a full disk can make writes hang.
If the same curl is fast, the problem is between the two: a wrong proxy_pass address, or DNS. Nginx resolves a name in proxy_pass only at startup, so a backend that has changed address needs a reload.
4. PHP: fastcgi_read_timeout
PHP through FastCGI uses its own setting, not the proxy_ ones:
location ~ \.php$ { include fastcgi_params; fastcgi_pass unix:/run/php/php8.3-fpm.sock; fastcgi_read_timeout 300s;}PHP also has its own limits, so raise them together, or PHP stops first:
; php.inimax_execution_time = 300
; the pool, for example /etc/php/8.3/fpm/pool.d/www.confrequest_terminate_timeout = 300Then reload both:
sudo nginx -t && sudo systemctl reload nginxsudo systemctl reload php8.3-fpmIf it fails under load only, see whether the pool is exhausted. A log line such as server reached pm.max_children setting in the PHP-FPM log says so.
5. The 504 comes from further out
There’s often more than one proxy, and each has its own limit. The shortest one wins, so raising Nginx’s does nothing if another is smaller:
| In front of Nginx | Limit | Looks like |
|---|---|---|
| Cloudflare | About 100 seconds, on most plans | 524 with Cloudflare’s own page |
| AWS Application Load Balancer | Idle timeout, 60 seconds by default | 504 from awselb/2.0 |
| Kubernetes Ingress NGINX | 60 seconds by default | Set with the proxy-read-timeout annotation |
To see who answered, check the Server header:
curl -sI https://example.com/report | grep -i '^server'6. Raising it the right way
Raise it where it’s needed, not everywhere. A long global timeout lets slow requests tie up your workers across the site:
location /reports/ { proxy_pass http://127.0.0.1:3000; proxy_read_timeout 300s;}The timeout is the wait between two reads, not for the whole request, so an answer that keeps sending data doesn’t hit it. WebSockets and long-lived streams need a long one too. The Nginx Config Generator sets this for WebSocket proxies.
Then, for work that really takes minutes, consider not doing it during the request at all. Accept the job, answer 202 straight away, run it in the background, and let the client check for the result. No proxy can time that out, and a retry doesn’t start the work twice.
Warning
Raising a timeout doesn’t stop anything. When the proxy gives up, the backend usually keeps working, and a visitor who reloads starts the same heavy work again. That’s how a slow page turns into an outage, so limit what a slow request can cost as well.
Shell, we have a problem!
Similar, but different: a 502 Bad Gateway means the backend refused or broke the connection, instead of staying silent. A 503 means there’s no backend available, or a rate limit. A 413 is about upload size. And a request that fails with ERR_TOO_MANY_REDIRECTS is a redirect loop.
Found this useful? Share it.
Found a mistake? Let us know at contact@ohsudo.com.