Troubleshooting Playbook: Diagnosing 502 and 504 Gateway Errors on a VPS

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 lineMeaningJump to
connect() to unix:/run/php/php8.4-fpm.sock failed (2: No such file)Wrong socket path or FPM is downCause A
connect() ... failed (13: Permission denied)Socket ownership / SELinux / AppArmorCause B
recv() failed (104: Connection reset by peer)Worker died mid-request — usually OOM or request_terminate_timeoutCause C
upstream timed out (110: Connection timed out)504 — upstream slower than fastcgi_read_timeoutCause D
no live upstreamsAll pool workers busy or deadCause 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.log and match the exact string — do not skip this.
  • Check systemctl status php8.4-fpm and socket existence.
  • Check dmesg for OOM kills.
  • Check information_schema.processlist for 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.1 and alert on listen-queue depth.
  • Set request_terminate_timeout below fastcgi_read_timeout so PHP reports the failure before nginx does.
  • Alert on OOM kills — they are silent until someone reports a 502.

Leave a Reply