A 502 means nginx reached the upstream but got a bad answer, or could not connect at all. A 504 means nginx gave up waiting for an answer. They look similar in a browser and completely different in a log, and the first two minutes of diagnosis determine whether you fix it in five minutes or fifty. This playbook is a decision tree for the common causes on a self-managed VPS running nginx + PHP-FPM + MariaDB.
Minute 0: Read the Error Log, Not the Access Log
tail -50 /var/log/nginx/error.log
journalctl -u php8.4-fpm --since '10 min ago' --no-pager | tail -50
dmesg -T | tail -20 # watch for OOM kills
| Error log line | Meaning | Jump to |
|---|---|---|
connect() to unix:/run/php/php8.4-fpm.sock failed (2: No such file) | Wrong socket path or FPM is down | Cause A |
connect() ... failed (13: Permission denied) | Socket ownership / SELinux / AppArmor | Cause B |
recv() failed (104: Connection reset by peer) | Worker died mid-request — usually OOM or request_terminate_timeout | Cause C |
upstream timed out (110: Connection timed out) | 504 — upstream slower than fastcgi_read_timeout | Cause D |
no live upstreams | All pool workers busy or dead | Cause E |
Match the line you actually see. Guessing which cause applies is how a five-minute fix becomes an afternoon.
Cause A: Socket Path Mismatch (Most Common After an Upgrade)
Ubuntu upgrades from PHP 8.3 to 8.4 create a new socket path while your nginx config still points at the old one. Confirm what exists and what nginx expects, then reconcile them:
ls -l /run/php/
grep -r fastcgi_pass /etc/nginx/
php-fpm8.4 -tt 2>&1 | grep -i listen
The durable fix is to pin the pool to a version-independent path so the next upgrade does not repeat the outage:
# /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
Cause B: Permission Denied on the Socket
If nginx runs as www-data and FPM’s socket is owned by root:root with mode 0660, every request fails with the same 502. Check without guessing:
ps -o user= -C nginx | sort -u
stat -c '%U %G %a' /run/php/php-fpm.sock
sudo -u www-data test -w /run/php/php-fpm.sock && echo OK || echo DENIED
The listen.owner / listen.group / listen.mode directives above fix the common case. On systems with SELinux enforcing, the socket also needs the httpd_sys_rw_content_t context — check sudo ausearch -m avc -ts recent before blaming permissions.
Cause C: Workers Dying (OOM or Terminate Timeout)
A connection reset almost always means a worker process was killed. Two candidates: the kernel OOM killer, or FPM’s own request_terminate_timeout.
journalctl -k | grep -i 'killed process'
grep -i 'execution timed out' /var/log/php8.4-fpm.log
free -m
If the OOM killer is involved, the pool is oversubscribed. Cap the FPM pool so that pm.max_children × memory-per-worker stays inside available RAM minus the database. On a 2 GB VPS with 45 MB workers and a 700 MB MariaDB buffer pool, that is roughly max_children = 18, not the 50 many guides suggest. The full derivation is in tuning PHP-FPM pools by workload type.
Cause D: The 504 That Is Really a Slow Query
Raising fastcgi_read_timeout from 60 s to 300 s is the wrong fix — it converts a visible error into a hung worker that blocks the whole pool. Find out what the request was waiting for:
# what was running at the moment of timeout
mysql -e "SELECT id, time, state, LEFT(info,80) FROM information_schema.processlist
WHERE time > 5 ORDER BY time DESC LIMIT 10;"
# PHP slow log, if enabled
tail -100 /var/log/php8.4-fpm-slow.log
A long-running SELECT ... WHERE with state = Sending data points at a missing index or a buffer pool that no longer covers the working set. A Waiting for table metadata lock points at a concurrent ALTER TABLE. Neither is fixed in nginx. The classification method in our PHP-FPM slow-log profiling guide will tell you which class you are in within one request.
Cause E: No Live Upstreams (Pool Exhaustion)
When every worker is busy, nginx cannot even hand off the request. Confirm the pool is saturated rather than crashed:
# FPM status page: enable pm.status_path = /fpm-status first
curl -s http://127.0.0.1/fpm-status?full | \
grep -E 'active processes|max active|listen queue|max children reached'
listen queue above zero means requests are already waiting. max children reached confirms the pool ceiling is the binding constraint. If both are true and CPU is not saturated, your bottleneck is downstream (database or network I/O) and a bigger pool will make it worse, not better.
The Order of Operations
- Read
nginx/error.logand match the exact string — do not skip this. - Check
systemctl status php8.4-fpmand socket existence. - Check
dmesgfor OOM kills. - Check
information_schema.processlistfor long queries. - Only then consider timeouts or pool sizes.
- Fix the root cause; a timeout bump is a painkiller, not a cure.
A stable 502/504-free stack needs the right instance size underneath it. If you are repeatedly hitting pool or memory ceilings after correct tuning, the workload has outgrown the tier — our VPS configuration comparison shows where the memory and vCPU steps fall.
Prevent the Next One
- Pin the FPM socket path so PHP upgrades cannot break the vhost.
- Enable the FPM status page bound to
127.0.0.1and alert on listen-queue depth. - Set
request_terminate_timeoutbelowfastcgi_read_timeoutso PHP reports the failure before nginx does. - Alert on OOM kills — they are silent until someone reports a 502.




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