A 502 Bad Gateway from nginx in front of PHP-FPM tells you very little. nginx tried to hand the request to PHP and did not get a usable answer. The cause is in /var/log/nginx/error.log, and the useful part is the number in parentheses. That is the errno returned by the failed connect() call. Each value points to a different fix. This article covers Debian 13 (trixie) with nginx and php8.4-fpm. It maps every errno to its cause and gives you the commands to confirm it before you change anything.
The error messages in nginx's error.log
Run sudo tail -n 50 /var/log/nginx/error.log and look for one of these messages. Timestamps, PIDs and client details are trimmed here:
connect() to unix:/run/php/php8.4-fpm.sock failed (13: Permission denied) while connecting to upstream
connect() to unix:/run/php/php8.4-fpm.sock failed (2: No such file or directory) while connecting to upstream
connect() failed (111: Connection refused) while connecting to upstream
upstream timed out (110: Connection timed out) while reading response header from upstream
To pull out only the upstream errors:
sudo grep -E 'connect\(\)|upstream timed out' /var/log/nginx/error.log | tail -n 20
Decision table: errno to fix
| errno in error.log | Meaning | Typical cause on Debian 13 | First check | Fix |
|---|---|---|---|---|
| 13: Permission denied | nginx cannot write to the socket | listen.owner/listen.group/listen.mode do not let www-data in | namei -l on the socket | Set the listen.* options or listen.acl_users, then restart FPM |
| 2: No such file or directory | No socket at that path | Old path such as php8.3-fpm.sock or php7.4-fpm.sock, or FPM is not running | ls -l /run/php/, nginx -T | Fix fastcgi_pass or start FPM |
| 111: Connection refused | Nothing is listening | nginx uses TCP but the pool uses a socket (or the reverse), or a stale socket file is left behind | ss -tlnp, ss -xlp | Make the listen and fastcgi_pass addresses match, or restart FPM |
| 110: Connection timed out | PHP did not answer in time (nginx returns 504, not 502) | Slow script, or all workers busy (pm.max_children) | FPM log, slowlog, status page | Fix the slow code, size the pool, adjust timeouts |
Quick fix for the most common 502 Bad Gateway cause: wrong socket path
The most common cause on a new or upgraded Debian 13 box is a fastcgi_pass line that points to a socket that does not exist. Debian 13's own sample site, /etc/nginx/sites-available/default, still contains a commented example with unix:/run/php/php7.4-fpm.sock. Uncomment it as it is and you get errno 2. Compare what nginx uses with what exists:
sudo nginx -T 2>/dev/null | grep -n fastcgi_pass
ls -l /run/php/
Debian's PHP 8.4 packaging expects the socket at /run/php/php8.4-fpm.sock. Change the fastcgi_pass line in your site file:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}
Then test and reload. A reload keeps existing connections open. If nginx -t fails, the reload does not run:
sudo nginx -t && sudo systemctl reload nginx
Diagnosing each errno
13: Permission denied – socket ownership vs. the nginx user
Connecting to a Unix stream socket requires write permission on the socket file (see unix(7)). The connecting user also needs search (x) permission on every directory in the path. On Debian, nginx workers run as www-data, as set by user www-data; in /etc/nginx/nginx.conf. Check that first:
grep -E '^\s*user' /etc/nginx/nginx.conf
ps -o user=,pid=,args= -C nginx
namei -l shows the mode and owner of every part of the path, so you can see exactly where access stops:
namei -l /run/php/php8.4-fpm.sock
On a stock install the output looks like this. Debian's tmpfiles entry creates /run/php as 0755 www-data:www-data, and the socket's default mode is 0660:
f: /run/php/php8.4-fpm.sock
drwxr-xr-x root root /
drwxr-xr-x root root run
drwxr-xr-x www-data www-data php
srw-rw---- www-data www-data php8.4-fpm.sock
A quick test as the nginx user:
sudo -u www-data test -w /run/php/php8.4-fpm.sock && echo writable || echo NOT writable
Errno 13 usually shows up after someone adds a per-site pool that runs as its own user. They also copy that user into the listen.* settings:
grep -HE '^\s*(user|group|listen)' /etc/php/8.4/fpm/pool.d/*.conf
/etc/php/8.4/fpm/pool.d/shop.conf:user = shop
/etc/php/8.4/fpm/pool.d/shop.conf:group = shop
/etc/php/8.4/fpm/pool.d/shop.conf:listen = /run/php/php8.4-fpm-shop.sock
/etc/php/8.4/fpm/pool.d/shop.conf:listen.owner = shop
/etc/php/8.4/fpm/pool.d/shop.conf:listen.group = shop
The socket is owned by shop:shop with mode 0660, and www-data is in neither. The PHP manual says that when listen.owner is not set, the socket's user and group default to the pool's running user, so leaving the lines out gives the same result.
2: No such file or directory – wrong path or FPM not running
Three things to check, in this order:
systemctl status php8.4-fpm --no-pager
ss -xlp | grep php
sudo journalctl -u php8.4-fpm -n 50 --no-pager
- Service not running. Look in the journal for config errors. Run
sudo php-fpm8.4 -tto test the config. Use-ttto dump the parsed config as well. Debian builds FPM with/etc/php/8.4/fpmas its config directory, so no-ypath is needed. - Version mismatch.
ls /run/php/showsphp8.4-fpm.sock, but the site still saysphp8.3-fpm.sock. This happens after a PHP minor upgrade or a release upgrade. - Socket in
/tmp. Suppose the pool listens on something like/tmp/php.sockand you addedPrivateTmp=to either unit. Each service then gets its own/tmp, so nginx cannot see the socket. Keep sockets in/run/php.
Debian also maintains a version-neutral path. The php8.4-fpm.service unit runs /usr/lib/php/php-fpm-socket-helper in ExecStartPost. That helper reads the listen line from pool.d/www.conf and registers the socket as the php-fpm.sock alternative at /run/php/php-fpm.sock:
update-alternatives --display php-fpm.sock
111: Connection refused – TCP mismatch or stale socket
A fastcgi_pass 127.0.0.1:9000; line with no address after to in the log means nginx is trying TCP. Check whether anything listens there:
sudo ss -tlnp | grep -E ':9000\b'
sudo ss -xlp | grep php-fpm
If the TCP check finds nothing but the Unix check shows the FPM socket, the two configs disagree. Pick one transport and use it on both sides.
Errno 111 also occurs with Unix sockets. unix(7) documents ECONNREFUSED for a pathname that exists but has no listener behind it. You will see this if the FPM master was killed and the socket file was left behind. In that case ls -l /run/php/ shows the file, but ss -xlp shows no listener on it.
Stale processes after the DSA-6514-1 update
DSA-6514-1 fixed php8.4 in trixie with version 8.4.26-1~deb13u1. The update itself is covered in patching PHP-FPM for DSA-6514-1 on Debian 13. After any PHP security update, check that the running master is newer than the package and that it created the socket:
dpkg-query -W php8.4-fpm
grep ' upgrade php8.4-fpm' /var/log/dpkg.log | tail -n 1
systemctl show php8.4-fpm -p ActiveEnterTimestamp
ls -l --full-time /run/php/
sudo ss -xlp | grep php8.4-fpm.sock
Watch for a master that started before the upgrade, or a socket file with no listener. In either case, restart FPM (see the warning in the fixes section).
pm.max_children exhausted and 110: upstream timed out
When every worker is busy, new requests queue in the listen backlog. If PHP sends nothing within fastcgi_read_timeout (default 60s), nginx logs errno 110 and returns 504, not 502. These show up in the same incidents, so check the FPM log too. On Debian it is /var/log/php8.4-fpm.log:
sudo grep -E 'max_children|seems busy' /var/log/php8.4-fpm.log | tail -n 20
WARNING: [pool www] server reached pm.max_children setting (5), consider raising it
Debian's www.conf is based on the upstream template. It keeps pm = dynamic and pm.max_children = 5, but changes listen to /run/php/php8.4-fpm.sock. Five workers is low for anything beyond a small site. Before you raise it, measure how much memory each worker uses:
ps -C php-fpm8.4 -o pid=,rss=,args=
ps -C php-fpm8.4 -o rss= | awk '{s+=$1; n++} END {if (n) print n" procs, avg "int(s/n/1024)" MiB"}'
Divide the RAM you can give PHP by the average worker size. That is your upper limit for pm.max_children. If you raise it past what fits in memory, the 502s become OOM kills.
If workers are not maxed out and you still get 110, find the slow code instead of raising timeouts. Add request_slowlog_timeout and slowlog to the pool. FPM then writes a PHP backtrace for every request that runs longer than the threshold.
systemd sandboxing or AppArmor in the way
Debian's stock php8.4-fpm.service has no sandboxing directives, but drop-ins you add can break the socket. One example is ProtectSystem=strict without ReadWritePaths=/run/php. It makes /run read-only for FPM, so FPM cannot create its socket and nginx then logs errno 2. Another is InaccessiblePaths=/run/php on the nginx unit. Check what is actually loaded:
systemctl cat php8.4-fpm nginx
sudo aa-status
sudo journalctl -k --since -1h | grep -i 'apparmor="DENIED"'
If you are hardening these units on purpose, the systemd sandboxing guide for Debian 13 covers allow-listing paths with ReadWritePaths=.
Fixes for nginx 502 Bad Gateway with PHP-FPM
Fix permissions (errno 13)
Keep the pool running as its own user, but give the socket to the web server:
[shop]
user = shop
group = shop
listen = /run/php/php8.4-fpm-shop.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
You can use POSIX ACLs instead (Debian builds FPM with ACL support): set listen.acl_users = www-data. When it is set, listen.owner and listen.group are ignored. Do not "fix" errno 13 with listen.mode = 0666. That lets every local user run PHP as the pool user.
Warning: restarting PHP-FPM drops requests that are in progress, and a broken config leaves the service down. Always test first:
sudo php-fpm8.4 -t && sudo systemctl restart php8.4-fpm
namei -l /run/php/php8.4-fpm-shop.sock
Fix paths and transport (errno 2 and 111)
Make listen in the pool and fastcgi_pass in nginx byte-for-byte identical. If you need TCP (FPM on another host or in a container), bind it narrowly. The PHP manual warns that an exposed FastCGI port allows arbitrary code execution:
listen = 127.0.0.1:9000
listen.allowed_clients = 127.0.0.1
For a stale socket or a master from before an update, a restart of php8.4-fpm with the warning above is the fix.
Fix capacity and timeouts (110, max_children)
Raise pm.max_children only within your memory budget. Raise fastcgi_read_timeout only for locations that really need long requests, such as exports or imports. Do not raise it globally. Keep request_terminate_timeout in the pool higher than or equal to the nginx timeout. Otherwise FPM kills the worker while nginx is still waiting, and the client gets a 502 instead of a 504.
Verifying the fix with curl and cgi-fcgi
First, test through nginx:
curl -sS -o /dev/null -w '%{http_code}\n' -H 'Host: example.com' http://127.0.0.1/index.php
Then test PHP-FPM directly, without nginx. Install libfcgi-bin (it provides cgi-fcgi) and enable the ping endpoint in the pool with ping.path = /ping. Its default response is pong. Restart FPM afterwards, with the warning above. Running the check as www-data also tests socket permissions:
sudo apt install libfcgi-bin
sudo -u www-data env SCRIPT_NAME=/ping SCRIPT_FILENAME=/ping REQUEST_METHOD=GET cgi-fcgi -bind -connect /run/php/php8.4-fpm.sock
A few response headers followed by pong means the socket, permissions and FPM master are fine. If you still get a 502, the problem is in nginx's config. Set pm.status_path = /status as well and use the same call to read the max children reached counter. Do not expose /ping or /status through a public nginx location. The web server security basics post covers restricting internal endpoints.
Preventing it
- On single-PHP hosts, point nginx at Debian's
/run/php/php-fpm.sockalternative so minor PHP upgrades do not breakfastcgi_pass. If you run several PHP versions side by side, use the versioned path explicitly. The alternative follows priority, not your intent. - Write
listen.owner,listen.groupandlisten.modeexplicitly in every pool file you create. - Run
php-fpm8.4 -tandnginx -tin every deploy and config-management run before restarting or reloading. - After every PHP DSA, check the service start time and
ss -xlp. - Monitor the FPM ping endpoint and alert on
server reached pm.max_childrenin/var/log/php8.4-fpm.log.
Takeaways checklist
- Read the errno in
/var/log/nginx/error.logbefore you change anything. - 13: run
namei -lon the socket, then fixlisten.owner/listen.group/listen.modeforwww-data. - 2: compare
nginx -T | grep fastcgi_passwithls /run/php/andsystemctl status php8.4-fpm. - 111: compare
ss -tlnpwithss -xlp, and look for stale sockets or old masters after updates. - 110 / max_children: check
/var/log/php8.4-fpm.log, size workers by RSS, and use the slowlog. - Check
systemctl catdrop-ins and AppArmor denials. - Verify with
curlandcgi-fcgirun aswww-data.
Sources
- PHP Manual: FPM configuration directives
- Debian packages: php8.4-fpm in trixie
- Debian Security Tracker: DSA-6514-1
- Debian php8.4 source: debian/php-fpm.service
- Debian php8.4 source: debian/php-fpm.tmpfile
- Debian php8.4 source: debian/php-fpm.conf (Apache FPM socket config)
- Debian php8.4 source: 0011-fpm-config.patch
- Debian php8.4 source: debian/rules
- php8.4 source: sapi/fpm/www.conf.in
- Debian php-defaults 96: php-fpm-socket-helper
- Debian nginx 1.26.3-3+deb13u7: sites-available/default
- Debian nginx 1.26.3-3+deb13u7: nginx.conf
- nginx: ngx_http_fastcgi_module
- nginx: Command-line parameters
- php-src PHP-8.4: fpm_process_ctl.c
- php-fpm8.4(8) man page (Debian trixie)
- namei(1) man page (Debian trixie)
- ss(8) man page (Debian trixie)
- unix(7) man page (Debian trixie)
- systemd.exec(5) man page (Debian trixie)
- Debian packages: libfcgi-bin in trixie
Comments