
Table of Contents
When Nginx returns 504, PHP-FPM is not replying within the timeout. Open the FPM slow log and Nginx error log first, then check pool children and backlog. You know it is fixed when the 504 stops, request_slowlog_timeout entries disappear, and pm.max_children no longer maxes out under load. Verify with curl timing and a fresh log tail after a reload.
Symptom and Quick Triage Commands
A 504 Gateway Timeout appears when Nginx cannot get a response from upstream PHP-FPM inside fastcgi_read_timeout. Start with these commands on a typical 2026 Ubuntu 24.04 or Debian 12 box running Nginx 1.26 and PHP 8.3.
# tail nginx error log for upstream timeouts
tail -f /var/log/nginx/error.log | grep -i 'upstream timed out'
# check php-fpm status and slow log
tail -f /var/log/php8.3-fpm.log
tail -f /var/log/php-fpm/www-slow.log
# see active children vs max
grep 'max_children' /var/log/php8.3-fpm.log
# measure a request end to end
curl -o /dev/null -s -w '%{time_total}n' https://example.com/
If time_total is above your fastcgi_read_timeout (default 60s), PHP is the bottleneck. If curl is fast but the browser shows 504, check Nginx proxy layers or Cloudflare in front.
Step-by-Step Process to Isolate the Root Cause
Follow this ordered sequence every time a 504 hits production. Do not jump to raising timeouts.
- Confirm the 504 source with
tail /var/log/nginx/error.logand note the upstream IP and port. - Read
/var/log/php-fpm/www-slow.logfor stack traces of the stuck script. - Check FPM pool occupancy with
pm.status_pathand compare topm.max_children. - Profile slow queries in MySQL slow log or with Query Monitor for WordPress hooks.
- Test external HTTP calls the request makes with
curl -m 5to the third party. - Apply the real fix, reload FPM and Nginx, then re-run the curl timing test.
Rollback Pitfall
If you raise fastcgi_read_timeout to 300s and latency gets worse because children stay busy longer, revert the change with sudo nginx -s reload after restoring the old value from backup. Never edit without a copy: cp nginx.conf nginx.conf.bak.
504 vs 502 vs 499
These three codes are often confused. 504 means Nginx waited for PHP-FPM and gave up; PHP was reachable but too slow or stuck. 502 Bad Gateway means Nginx could not connect at all — FPM is down, the socket path is wrong, or the pool crashed. 499 is a client-side code: the browser or proxy closed the connection before Nginx responded, common behind Cloudflare when the visitor navigates away.
To tell them apart, check the Nginx access log format. A 504 shows upstream time in the error log. A 502 shows connect() failed. A 499 appears only in the access log with zero upstream time. When debugging WordPress, also review our notes on what Redis fixes in the WordPress object cache because a cold cache can turn a 200 into a 504 under load.
Get a Quote
Email + requirement + budget range — we reply with a quote within 24 hours.
Slow Queries and Slow HTTP Calls
A 504 is usually a slow request, not a broken one. Two sources dominate: database queries and outbound HTTP calls via WP_Http or wp_remote_get().
Reading the Slow Log
Enable the FPM slow log in the pool file /etc/php/8.3/fpm/pool.d/www.conf:
slowlog = /var/log/php-fpm/www-slow.log
request_slowlog_timeout = 5s
request_slowlog_trace_depth = 20
After a reload, a stuck request writes a PHP stack trace. You will see the exact function, for example wpdb::query() or a curl_exec inside a plugin. That trace is your map.
Database Side
Enable the MySQL slow log in /etc/mysql/mysql.conf.d/mysqld.cnf with slow_query_log = 1 and long_query_time = 2. Then use mysqldumpslow -s t /var/log/mysql/slow.log to rank by time. In WordPress, a missing index on postmeta or a meta_query with no cache is the usual suspect.
External HTTP
Plugins calling payment gateways or license servers block the FPM child for the full timeout. Reproduce with curl -m 5 -v https://api.vendor.com/ping. If it hangs, set wp_remote_get($url, ['timeout' => 5]) in code or move the call to a cron job with Action Scheduler. Verification: after the change, www-slow.log stops logging that trace and curl total time drops.
FPM Pool and Backlog
Even a fast script 504s if no child is free. The pool config decides capacity. In www.conf check pm = dynamic, pm.max_children = 50, pm.start_servers = 5, pm.max_requests = 500.
Reading Pool Exhaustion
The FPM log says server reached pm.max_children setting. Enable the status page to see live numbers:
pm.status_path = /fpm-status
Then curl http://localhost/fpm-status shows active processes and listen queue. If listen queue is above zero, the OS backlog is filling. Raise listen.backlog to 511 in the pool and net.core.somaxconn with sysctl -w net.core.somaxconn=511.
Why Children Stick
A child stuck in curl_exec or a lock wait cannot serve others. If you use Unix socket, check listen = /run/php/php8.3-fpm.sock and Nginx fastcgi_pass unix:/run/php/php8.3-fpm.sock;. A mismatch here causes 502, not 504, so confirm the code first. For high traffic sites we build at Custom WordPress Development, tuning max_children to RAM divided by average child size is standard practice.
Verification and Rollback
After changing max_children, run sudo php-fpm8.3 -t then systemctl reload php8.3-fpm. Watch htop for swap; too many children cause OOM. If the server swaps, lower the number and roll back the file from your backup.
Stopgap vs Real Fix

A stopgap keeps the site up now; the real fix removes the cause. Raising fastcgi_read_timeout from 60s to 300s is a stopgap — the visitor waits but the child is still stuck. The real fix is the slow query index, the 5-second outbound timeout, or moving the task to a queue.
Stopgaps are valid during an incident: proxy_read_timeout 300s; in the Nginx location block plus a maintenance page. But pair it with a ticket. Real fixes are verified by load testing with ab -n 100 -c 10 https://example.com/ and confirming zero 504 and a flat slow log. If you run a foreign trade store, see Server for Foreign Trade Website: Vendor Guide 2026 for sizing that avoids these pools entirely.
Prevention and Monitoring
Set request_slowlog_timeout = 5s permanently, ship logs to a central store, and alert on max_children in the FPM log. Use pm.max_requests to recycle children and catch memory leaks. Keep Nginx and PHP patched; in 2026 PHP 8.3 and 8.4 are current, and Nginx 1.26 is stable.
Action Checklist
- Tail Nginx error log and FPM slow log at first 504 report.
- Enable
request_slowlog_timeoutandpm.status_pathbefore the next incident. - Fix the slow query or outbound call shown in the trace, not just the timeout.
- Size
pm.max_childrento RAM and confirm withabload test. - Keep a backup of every conf file for one-command rollback.
Frequently asked questions
Is a 504 always a PHP-FPM problem?
No. It can be a reverse proxy, a database lock, or an external API. The upstream IP in the Nginx error log tells you which layer timed out.
How do I know the pool is too small?
When active processes equals pm.max_children and listen queue is above zero on the FPM status page, the pool is exhausted.
Should I just increase the timeout to stop 504?
Only as a temporary stopgap. The real fix is the slow code or query found in the slow log; otherwise children stay busy and the site gets slower.
Need a team that tunes Nginx and PHP-FPM for WordPress every day? See our development services and ship without 504s.
