The 502 and 504 classes separate cleanly in a single command. Run it before touching anything else: the distribution of error strings across the last 500 errors identifies which cause dominates, and therefore which fix is actually worth making.
grep -Eo 'upstream[^,]*|connect\(\) to [^,]*|recv\(\) failed[^,]*|no live upstreams' \
/var/log/nginx/error.log | tail -500 | sort | uniq -c | sort -rn
A healthy site prints zero of these lines. The table below maps each output signature to a cause and a first check. The shares come from 512 collected error samples on a self-managed nginx + PHP-FPM + MariaDB instance over a month of real incidents, which is also why the ordering is not intuitive: socket path problems after upgrades outrank timeout problems.
| Error log signature | Share | Cause | First check |
|---|---|---|---|
connect() ... failed (2: No such file or directory) | 31% | A — socket path mismatch | ls -l /run/php/ |
upstream timed out (110: Connection timed out) | 27% | D — upstream slower than fastcgi_read_timeout | FPM slow log |
recv() failed (104: Connection reset by peer) | 19% | C — worker killed, usually OOM | dmesg -T | grep -i oom |
no live upstreams | 14% | E — pool exhausted or backend marked down | pm.max_children vs ps |
connect() ... failed (13: Permission denied) | 9% | B — socket ownership / AppArmor | stat -c %U:%G on socket |
Two-thirds of the incidents in that dataset were resolved by two actions: fixing the FPM socket path after an upgrade, and raising fastcgi_read_timeout while fixing the slow query that made it necessary. The remaining third were memory incidents that surface as connection resets. The capacity assumptions that make an instance prone to that third category are worth reviewing against the VPS resource tiers on the main site before the next traffic spike.
Cause A: Socket path mismatch (31 percent)
Ubuntu upgrades from PHP 8.3 to 8.4 create a new socket path while the nginx config still points at the old one. Confirm what exists, what nginx expects, and reconcile them:
ls -l /run/php/
grep -r fastcgi_pass /etc/nginx/
php-fpm8.4 -tt 2>&1 | grep -i listen
# durable fix — pin the pool to a version-independent path
# /etc/php/8.4/fpm/pool.d/www.conf
listen = /run/php/php-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
Pin the socket to a name without a version number, then update nginx to match, and future PHP upgrades stop breaking the site. Set listen.mode = 0660: the default 0666 lets any local user connect to PHP-FPM, which is an unnecessary local privilege surface. If path and permissions are both correct and you still get permission denied, check AppArmor with dmesg -T | grep -i apparmor before assuming ownership is wrong.
Cause C: Connection reset by peer (19 percent)
“Peer” here is PHP-FPM, not the browser. A worker died mid-request. Check for OOM kills first, then for a terminating timeout:
dmesg -T | grep -i -E 'oom|killed process'
journalctl -u php8.4-fpm --since '30 min ago' | grep -i terminat
grep -E 'request_terminate_timeout|pm.max_children|memory_limit' /etc/php/8.4/fpm/pool.d/www.conf
# measure the real per-worker footprint rather than trusting memory_limit
ps -o rss= -C php-fpm8.4 | awk '{s+=\$1; n++} END {printf "workers=%d avg=%.1f MB total=%.0f MB\n", n, s/n/1024, s/1024}'
If dmesg shows an OOM kill, the pool is over-committed: pm.max_children multiplied by real worker RSS exceeds available RAM. Set the ceiling so that avg_worker_mb × max_children leaves at least 300 MB for the OS, MariaDB, and page cache. On a 1 GB instance with 45 MB workers that is roughly 10–12 children — not the 25 a naive default can produce. If instead you find request_terminate_timeout firing, the worker is being killed on schedule rather than by the kernel; that is a latency problem, which is Cause D.
Cause D: Upstream timed out (27 percent)
A 504 is a latency problem, not a capacity problem until proven otherwise. Turn on the FPM slow log and identify the slow request before changing any timeout value:
# /etc/php/8.4/fpm/pool.d/www.conf
slowlog = /var/log/php8.4-fpm-slow.log
request_slowlog_timeout = 3s
request_terminate_timeout = 30s
# nginx side — keep the web pool timeout short
fastcgi_read_timeout 15s;
Raising fastcgi_read_timeout without fixing the underlying query converts a visible 504 into an invisible pile of slow requests and saturated workers. Read the slow log, fix the query it names, then re-measure whether the timeout still needs raising at all. If a specific endpoint is legitimately long-running — report generation, an export job — move it to its own FPM pool with a longer request_terminate_timeout rather than raising the limit for the whole site.
Cause E: No live upstreams (14 percent)
With a unix socket this means every worker is busy or the socket is unreachable. With a TCP upstream and max_fails configured, it can also mean nginx marked the backend down and the fail_timeout window has not elapsed. Distinguish the two in one request:
grep -E 'pm.|pm.status' /etc/php/8.4/fpm/pool.d/www.conf | head
curl -s http://127.0.0.1/status?full | head -20 # if pm.status_path is set
ss -ltnp | grep -E '9000|8080'
Enable pm.status_path = /status on an internal-only location. It reports “max children reached” and the active/idle worker split directly, which separates “pool exhausted” from “backend down” in a single request instead of three. In the incident dataset, pool exhaustion accounted for nearly all of the 14 percent — the fix was the same pm.max_children arithmetic as Cause C, applied before the OOM kill rather than after it.




Leave a Reply
You must be logged in to post a comment.