504 Gateway Timeout means Nginx forwarded a request to an upstream — PHP-FPM, an app server, a container — and gave up waiting for the response. By default Nginx waits 60 seconds. The fix is either to give legitimately long requests more time, or (more often) to find out why requests are that slow.
Step 1: confirm in the error log
sudo tail -n 30 /var/log/nginx/error.log
upstream timed out (110: Connection timed out) while reading response header from upstream,
client: 198.51.100.7, server: example.com, request: "POST /wp-admin/admin-ajax.php HTTP/2.0",
upstream: "fastcgi://unix:/run/php/php8.3-fpm.sock"
Note the request (which URL is slow) and the upstream (which backend). If only one URL times out, it’s that operation; if everything does, the backend is overloaded or stuck.
Step 2: decide — slow by design, or a problem?
Some requests legitimately take long: large exports, report generation, imports, backups triggered from an admin panel. Others shouldn’t: a homepage taking 60 seconds is a bug or overload.
Raising timeouts (for legitimately long requests)
Reverse proxy (Node.js, Python, Docker…)
location /admin/export {
proxy_pass http://127.0.0.1:3000;
proxy_connect_timeout 10s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
proxy_read_timeout is the one that matters most: the maximum time between two reads from the upstream (not the total request time). Scope long timeouts to the locations that need them rather than the whole site.
PHP-FPM
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_read_timeout 300s;
}
PHP has its own limits, and the lowest limit wins:
max_execution_timeinphp.ini(default 30 s; doesn’t count time spent waiting on the database on Linux).request_terminate_timeoutin the PHP-FPM pool config (default 0 = off). If set, PHP-FPM kills the worker — Nginx then reports 502, not 504.
; /etc/php/8.3/fpm/conf.d/99-timeouts.ini
max_execution_time = 300
Restart PHP-FPM and reload Nginx after changes.
Other layers
If there’s anything in front of Nginx, it has its own timeout: Cloudflare’s proxy returns 524 after 100 seconds on standard plans; load balancers default to 60 seconds. Raising Nginx’s timeout won’t help past those limits — long jobs should run in the background instead.
Fixing the real cause (most cases)
The backend is overloaded
All PHP-FPM workers busy → requests wait in a queue → timeouts. Check:
sudo grep "max_children" /var/log/php8.3-fpm.log | tail
top # are CPUs pegged? is the server swapping?
Fixes: page caching (most requests never reach PHP), more workers if RAM allows, or more CPU/RAM. See PHP-FPM tuning.
Slow database queries
The application waits on MySQL or PostgreSQL. Enable the slow query log and look for queries without indexes:
sudo mysql -e "SHOW FULL PROCESSLIST;"
See the MySQL slow query log and database indexing basics.
Slow external calls
An app waiting on a third-party API that hangs will time out too. Set client-side timeouts in your code (e.g. 5–10 seconds for HTTP calls) so failures are fast and visible.
Bots hammering expensive URLs
Search pages, filters, admin-ajax.php or xmlrpc.php under heavy bot traffic can tie up every worker. Check the access log for hot spots:
sudo awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
Rate-limit or block abusive patterns — see Nginx rate limiting.
Long-running work in a web request
Imports, video processing, sending thousands of emails — move these to a background queue (Laravel queues, Celery, BullMQ, WP-CLI via cron). The web request just starts the job and returns immediately.
Upstream not reachable at all?
If Nginx can’t even connect (firewall dropping packets, wrong IP), you’ll see connect() timed out and a 504 after proxy_connect_timeout. Check the address and that the upstream is listening — and see Nginx 502 Bad Gateway for connection errors.
Measure response times
Add upstream timing to your access log to find slow endpoints before users complain:
log_format timed '$remote_addr - [$time_local] "$request" $status '
'rt=$request_time urt=$upstream_response_time';
access_log /var/log/nginx/access.log timed;
awk -F'urt=' '{print $2, $0}' /var/log/nginx/access.log | sort -rn | head
Checklist
- Error log: which URL, which upstream?
- Legitimately long? Raise
proxy_read_timeout/fastcgi_read_timeoutfor that location, and PHP limits to match. - Everything slow? Check CPU, RAM, PHP-FPM worker saturation.
- Database: slow queries, locks.
- External APIs: client timeouts.
- Bots: rate limits.
- Heavy work: move to background jobs.
Constant timeouts under normal traffic usually mean the server is too small for the workload. Our VPS plans scale up to 14 vCPU and 40 GB RAM when caching and tuning are no longer enough.