Nginx returns 502 Bad Gateway when it hands a request to a backend and gets back nothing usable. Nginx itself is working fine — the upstream is the problem, and nginx has already written down exactly which one.
502 Bad Gateway
nginx/1.24.0
Step 1: Read the Error Log
Do this before changing anything. Every 502 writes a line to the nginx error log naming the upstream and the underlying errno:
sudo tail -n 50 /var/log/nginx/error.log
You are looking for a line like one of these:
connect() failed (111: Connection refused) while connecting to upstream, client: 203.0.113.9,
server: example.com, request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:3000/"
connect() to unix:/run/php/php8.3-fpm.sock failed (13: Permission denied)
while connecting to upstream
upstream sent too big header while reading response header from upstream
recv() failed (104: Connection reset by peer) while reading response header from upstream
no live upstreams while connecting to upstream
The number in parentheses is the actual fault. Match it below. If you are not sure where your logs live, see Where Are Nginx Logs Stored? — a custom error_log directive may have moved them.
Cause 1: (111: Connection refused) — Nothing Is Listening
The most common 502 by a wide margin. Nginx connected to an address where no process is accepting.
Check whether the backend is running:
sudo systemctl status php8.3-fpm # or your app's unit
sudo ss -ltnp # what is actually listening
ss -ltnp is the decisive command — it lists every listening TCP socket with the owning process. Compare that against your proxy_pass:
location / {
proxy_pass http://127.0.0.1:3000;
}
If nginx points at port 3000 and ss shows your app on 8000, that is your bug. If nothing is listening at all, start the backend:
sudo systemctl start php8.3-fpm
sudo systemctl enable php8.3-fpm # so it survives reboot
A subtle variant: the backend binds to 127.0.0.1 but nginx connects to a different address, or the app listens only on IPv6 while nginx resolves localhost to IPv4. Use an explicit 127.0.0.1 on both sides rather than localhost to eliminate the ambiguity.
If the backend keeps dying, read its own log rather than nginx's. A process that starts, serves a few requests, and exits is a crash, not a configuration error.
Cause 2: (13: Permission denied) — Socket Permissions
Seen almost exclusively with PHP-FPM over a Unix socket. The socket exists but the nginx worker user cannot open it.
In your PHP-FPM pool file (/etc/php/8.3/fpm/pool.d/www.conf on Debian/Ubuntu):
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
Set listen.owner and listen.group to the user nginx runs as — www-data on Debian/Ubuntu, nginx on RHEL, Rocky, and AlmaLinux. Confirm which with:
ps -o user= -C nginx | sort -u
Then restart PHP-FPM (not nginx):
sudo systemctl restart php8.3-fpm
ls -l /run/php/php8.3-fpm.sock
Permissions on the parent directory matter too — nginx needs execute (x) on every directory in the path to reach the socket.
Cause 3: upstream sent too big header — Buffers Too Small
The backend returned response headers larger than nginx's buffer. Large Set-Cookie values, long JWTs, and verbose auth headers all trigger it. The defaults are small:
| Directive | Default | Context |
|---|---|---|
proxy_buffer_size | 4k or 8k | http, server, location |
proxy_buffers | 8 4k or 8 8k | http, server, location |
proxy_busy_buffers_size | 8k or 16k | http, server, location |
Raise them for a proxied backend:
proxy_buffer_size 16k;
proxy_buffers 4 16k;
proxy_busy_buffers_size 32k;
For PHP-FPM, the equivalent directives carry the fastcgi_ prefix:
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
Note that proxy_buffer_size covers the header only — that is why the error mentions "response header" specifically.
Cause 4: (104: Connection reset by peer) — The Backend Died Mid-Request
The upstream accepted the connection and then vanished. That is a crash, an OOM kill, or a worker limit.
dmesg -T | grep -i -E 'killed process|out of memory'
For PHP-FPM, check its log for the tell-tale saturation message:
sudo grep -i "max_children" /var/log/php8.3-fpm.log
server reached pm.max_children setting means you are out of workers — raise pm.max_children, but only if the box has the RAM for it. Each child consumes memory, and over-provisioning turns a 502 into an OOM kill.
Cause 5: SELinux (RHEL, Rocky, AlmaLinux)
SELinux blocks nginx from making outbound network connections by default, which looks like a connection failure with no obvious cause.
getenforce
sudo ausearch -m avc -ts recent | grep nginx
If you see AVC denials, allow the connection:
sudo setsebool -P httpd_can_network_connect 1
The -P makes it persist across reboots. Do not disable SELinux wholesale to fix one boolean.
502 vs 504: Not the Same Problem
| Code | Meaning | Governing directive |
|---|---|---|
| 502 Bad Gateway | Upstream refused, reset, or sent a malformed response | — |
| 504 Gateway Timeout | Upstream accepted the connection but did not answer in time | proxy_read_timeout (default 60s) |
If you are getting 504s, raising buffers will not help — the backend is slow, and the honest fix is making it faster rather than raising proxy_read_timeout until the symptom disappears.
Verify the Fix
sudo nginx -t # config syntax
sudo systemctl reload nginx # apply without dropping connections
curl -I http://127.0.0.1/ # expect 200, not 502
sudo tail -f /var/log/nginx/error.log # watch for new upstream errors
Always run nginx -t before reloading. A config with a typo will fail to reload and leave the old config running — better than nginx refusing to start, but you will be debugging a fix that never applied.
Prevention
- Enable the backend at boot with
systemctl enable. A large share of "502 after reboot" is a backend that was never enabled. - Monitor the upstream, not just nginx. A health check that only hits nginx reports green while every request 502s.
- Watch memory. OOM kills are the most common cause of intermittent 502s on small servers.
- Alert on error.log. These errors are logged the instant they happen — see Where Are Linux System Logs Stored? for wiring them into a central log pipeline.