
How to Fix 502 Bad Gateway in Nginx (Without Restarting Everything Blindly)
Mahmud Hasan
October 17, 2026
What a 502 actually means (and why your first instinct is wrong)
When nginx sits in front of your app — as a reverse proxy with proxy_pass, or handing PHP to PHP-FPM with fastcgi_pass — it's the middleman. It forwards the request upstream, waits for an answer, and passes it back. A 502 Bad Gateway means the upstream gave nginx garbage back, or nothing at all: connection refused, a socket file that doesn't exist, a process that crashed mid-response, or response headers too big for the buffers.
That's it. nginx did its job and told you so. So "restart nginx" fixes a 502 about as often as repainting a car fixes the engine. And two lookalikes to keep straight: a 504 Gateway Timeout means the upstream was just slow — nginx gave up waiting. A 503 is the server telling you it's overloaded. A 502 says the thing behind nginx is broken. Start there.
Step zero: read the log before you touch anything
The error log usually names the culprit on the first line. Reproduce the 502, then:
tail -f /var/log/nginx/error.log
Three strings cover most 502s in the wild:
connect() failed (111: Connection refused)— nothing is listening where nginx is knocking. Your app is down or the port is wrong.connect() to unix:/run/php/php8.2-fpm.sock failed (2: No such file or directory)— the socket path is wrong or PHP-FPM isn't running.connect() to unix:/run/php/php8.2-fpm.sock failed (13: Permission denied)— the socket exists but nginx isn't allowed to use it.upstream sent too big header while reading response header from upstream— the app answered, but its headers overflowed nginx's buffers.
The upstream errors log at error level, which the default config already captures — no tuning needed to see them. Read this first and you've skipped half the debugging most people do blindly.
Suspect one: the upstream simply isn't running
Bypass nginx entirely and ask the app directly:
curl -I http://127.0.0.1:3000
(Use whatever port your app actually listens on.) If you get Connection refused, this was never an nginx problem — your app died, and the 502 is just nginx reporting the death. Restart it where it actually lives, but read its logs before the restart:
# PM2
pm2 logs --lines 50
pm2 restart myapp
# systemd
journalctl -u myapp -n 50 --no-pager
systemctl restart myapp
# Docker
docker logs my-container --tail 50
docker restart my-container
Two silent killers worth checking while you're here: free -h (an out-of-memory kill leaves nothing in your app's logs — the kernel just takes it) and df -h (a full disk crashes everything that tries to write). If the app answers curl fine, the problem is nginx's address for it — keep going.
Suspect two: nginx is knocking on the wrong door
The most common config bug in the PHP world: fastcgi_pass and PHP-FPM's listen disagree. nginx says fastcgi_pass unix:/run/php/php8.2-fpm.sock; while the pool config says listen = 127.0.0.1:9000. Nothing connects, everything 502s. Check both sides in ten seconds:
grep fastcgi_pass /etc/nginx/sites-enabled/mysite
grep "^listen" /etc/php/8.2/fpm/pool.d/www.conf
Make them match. Two classic variants: you upgraded PHP and the socket path changed (php8.1-fpm.sock became php8.2-fpm.sock) while the nginx config still points at the old file — and localhost vs 127.0.0.1, which some stacks resolve to different listeners. Pick 127.0.0.1 everywhere and stop thinking about it. For proxy setups the equivalent sin is a port mismatch: app on 3001, nginx pointing at 3000.
Suspect three: the socket exists and nobody may touch it
If ls -la /run/php/php8.2-fpm.sock shows the socket and the log says Permission denied, nginx (usually running as www-data, or nginx on RHEL systems) can't read and write it. Fix this in the FPM pool config, not with a chmod you'll forget next deploy:
# /etc/php/8.2/fpm/pool.d/www.conf
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
Then reload PHP-FPM. On RHEL-family systems there's one more layer: SELinux can block the socket even with correct Unix permissions, so check ausearch -m avc -ts recent if the denial persists.
Suspect four: the upstream is alive and drowning
Intermittent 502s under traffic are a capacity problem, not a config problem. The mechanism is simple: PHP-FPM's worker pool is capped by pm.max_children. Each worker holds roughly 30–50 MB of RAM, so a 4 GB VPS with a gigabyte spoken for by the system can run about 60 workers, not 200. When every worker is busy, the next connection is refused — and a refused connection is a 502.
# /etc/php/8.2/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 50 # from RAM math, not optimism
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500 # recycle workers before they leak
And the buffer case from the log section: if the app's response headers overflow the defaults — big Set-Cookie storms, chunky proxied headers — raise them:
fastcgi_buffers 8 16k;
fastcgi_buffer_size 32k;
(A slow-but-alive backend is a different animal: upstream timed out in the log means a 504, not a 502 — that's where fastcgi_read_timeout / proxy_read_timeout belong.)
The two traps that survive a restart
Trap one: Docker's stale DNS. nginx resolves hostnames in proxy_pass once — at startup or reload. Redeploy your backend container, it gets a new IP, and nginx keeps talking to the dead one: 502 forever. Restarting nginx "fixes" it only because the reload re-resolves. The real fix forces runtime resolution with a variable and Docker's embedded DNS (127.0.0.11):
resolver 127.0.0.11 valid=5s ipv6=off;
resolver_timeout 3s;
set $backend "http://backend-api:3000";
location / {
proxy_pass $backend;
}
Trap two: the CDN cached your 502. Cloudflare caches the error page itself and keeps serving it after your origin recovers. Purge the CDN cache — or point DNS at the origin temporarily — before declaring the fix didn't work.
And one discipline that prevents the most embarrassing 502 of all: nginx -t before every reload. It prints the exact file and line number on failure. Then prefer systemctl reload nginx over restart — a reload applies config without dropping live connections.
The order, every time
- Tail the error log while reproducing. It names the suspect.
curlthe upstream directly. Refused means the app, not nginx.- Compare addresses:
fastcgi_pass/proxy_passagainst where the backend actually listens. - Check socket permissions (
listen.owner, not chmod). - Only then:
nginx -t, then reload — never restart blindly.
To stop seeing 502s at all: size pm.max_children from RAM math, alert on the error log instead of the status code (the log sees the failure first), and watch disk space and memory — the two resources whose exhaustion looks exactly like an app crash. A 502 is a diagnostic, not a mystery. Read it first, restart last, and it's a five-minute problem.
References
Comments
More in Web Development

Is the AI Bubble About to Burst? The Honest 2026 Answer
The IMF just put a number on the AI-bubble debate: an investment bust could shave roughly a point off global GDP by 2028. This week gave both sides fresh ammunition — here is the honest middle.
Read more
