A 502 Bad Gateway from Nginx means one thing: Nginx itself is fine, but the backend it was supposed to hand your request to answered with garbage, refused the connection, or never answered at all. The fix is almost never in Nginx's own config — it is in whatever sits behind it: PHP-FPM, your Node or Python app, or a misconfigured upstream block.
This guide walks you through the six causes that account for nearly every 502 on a real Ubuntu server. For each one you get the exact command that proves it is the culprit and the exact fix. Everything here is tested on Ubuntu 24.04 with the standard Nginx packages. You will need SSH access with sudo and a server where the 502 is currently happening, because step one of debugging is always the error log.
Prerequisites
- An Ubuntu 22.04 or 24.04 server with Nginx installed.
- Sudo (root) SSH access.
- The 502 error must be reproducible right now — even a single curl that triggers it.
- Basic comfort reading Nginx config files in
/etc/nginx/sites-available/.
Step 0: Read the Nginx error log first
Every 502 leaves a fingerprint in the error log. Before touching anything, open it and reproduce the error:
sudo tail -f /var/log/nginx/error.log
Then load the page (or curl -I https://yourdomain.com) and watch. You will see one of a handful of lines. The line tells you which cause below applies to you:
connect() failed (111: Connection refused) while connecting to upstream— the backend is not listening where Nginx expects (Causes 1, 2, 3).upstream timed out (110: Connection timed out)— the backend is too slow (Cause 4).upstream sent too big header while reading response header from upstream— header/buffer too small (Cause 5).connect() to unix:/run/php/php8.3-fpm.sock failed (13: Permission denied)— socket permissions (Cause 6).recv() failed (104: Connection reset by peer) while reading response header from upstream— the backend crashed mid-request (Cause 2).
Match your line to a cause below and follow that fix. If the log shows nothing, skip to the troubleshooting section.
Cause 1: PHP-FPM is down or Nginx is pointing at the wrong socket
This is the single most common 502 on WordPress, Laravel, and plain-PHP sites. Either PHP-FPM has crashed, or it listens on a socket that does not match the fastcgi_pass line in your Nginx config.
First, check whether PHP-FPM is running:
sudo systemctl status php8.3-fpm --no-pager
If it says active (running), move on. If it is dead, restart it:
sudo systemctl restart php8.3-fpm
sudo systemctl status php8.3-fpm --no-pager
Note: Ubuntu 24.04 ships PHP 8.3; on 22.04 it is 8.1 or 8.2, and the Ondrej PPA gives you 8.4. Adjust the version in the commands to whatever php -v reports on your box.
Now compare the socket Nginx expects with the socket PHP-FPM actually listens on. Check the Nginx side:
grep -rn "fastcgi_pass" /etc/nginx/sites-enabled/
You will see something like fastcgi_pass unix:/run/php/php8.3-fpm.sock; (socket) or fastcgi_pass 127.0.0.1:9000; (TCP). Then check the PHP-FPM pool:
grep -E "^listen" /etc/php/8.3/fpm/pool.d/www.conf
The two must match exactly. If the pool listens on /run/php/php8.3-fpm.sock but Nginx points at 127.0.0.1:9000, you get a 502 with "Connection refused". Fix the fastcgi_pass line to match the pool (or vice versa), test the config, and reload:
sudo nginx -t
sudo systemctl reload nginx
Also peek at the FPM log for crash clues — out-of-memory kills show up here:
sudo tail -n 50 /var/log/php8.3-fpm.log
Cause 2: Your app behind proxy_pass is down or crashing
For Node.js, Python (Gunicorn/uvicorn), Go, or Docker apps behind proxy_pass, a 502 usually means the app process is not listening on the expected port, or it crashes on the request. Prove it by hitting the backend directly, bypassing Nginx:
curl -v http://127.0.0.1:3000/
Replace 3000 with your app's port. If curl says "Connection refused", the app is simply not running — start it (PM2, systemd, or Docker) and confirm the port appears in the listener list:
sudo ss -tlnp | grep 3000
If curl connects but then drops with an empty reply, the app is crashing on this request. Check its own logs — for a PM2 app:
pm2 logs --lines 50
For a systemd service:
sudo journalctl -u your-app --since "10 min ago" --no-pager
A very common pattern: the app runs out of memory, the kernel's OOM killer shoots it, PM2/systemd restarts it, and users see a 502 for the few seconds it is down. Confirm with:
sudo dmesg | grep -i "killed process" | tail -n 10
If you see your app in that output, the fix is not Nginx tuning — it is giving the app more RAM (bigger server or lower worker count) so it stops being killed.
Cause 3: proxy_pass points at the wrong host or port
Even when the app runs fine, Nginx may be talking to the wrong address. A classic: your config uses a hostname that resolves to IPv6 first while the app only binds IPv4.
proxy_pass http://localhost:3000;
On many systems localhost resolves to ::1 (IPv6), and a Node app listening on 127.0.0.1 (IPv4) never sees the connection — 502. The error log confirms it with connect() failed (111: Connection refused). The fix is blunt and permanent: never use localhost in proxy_pass. Use the explicit address:
proxy_pass http://127.0.0.1:3000;
Then sudo nginx -t && sudo systemctl reload nginx.
The same class of bug hits upstream blocks with a stale port. Verify what you configured versus what is actually listening:
grep -rn "proxy_pass\|server 127" /etc/nginx/sites-enabled/ /etc/nginx/conf.d/
sudo ss -tlnp
Every proxy_pass http://127.0.0.1:PORT must have a matching line in the ss output. If your app moved ports during a redeploy and Nginx still points at the old one, that is your 502.
Cause 4: The backend is alive but too slow — upstream timeouts
Here curl to the backend works, but it takes 30, 60, or 90 seconds to answer — a heavy report, an unindexed query, a slow API call. Nginx gives up first and returns 502 (or 504). The log line is unmistakable:
upstream timed out (110: Connection timed out) while reading response header from upstream
Find which timeout fires by checking the request's real duration against your config. Defaults are 60 seconds for proxy_read_timeout and 60 for fastcgi_read_timeout. If your endpoint legitimately needs more, raise them for that location:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_connect_timeout 30s;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
For PHP-FPM long jobs, the matching knobs are fastcgi_read_timeout and fastcgi_send_timeout.
Raising timeouts hides slowness; it does not fix it. If a normal page takes 70 seconds, users still suffer — treat the timeout change as the bandage and then find the slow query or missing index. And keep PHP-FPM's request_terminate_timeout in sync or FPM will kill the script while Nginx keeps waiting.
Cause 5: Response headers too large — buffer limits
This one appears out of nowhere on login pages and apps that set many or very large cookies (SSO tokens, JWTs in headers). The error log says:
upstream sent too big header while reading response header from upstream
Nginx reads the backend's response headers into a fixed-size buffer. When the headers exceed it, 502. Fix by enlarging the buffers for proxy setups:
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
And the FastCGI equivalents for PHP-FPM sites:
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
Put them inside the affected server or location block, test, reload. Do not just crank these to megabytes everywhere — large values multiply by connection count and eat RAM. If headers are absurdly large (megabytes), the app has a bug; fix the app.
Cause 6: Socket permission denied — Nginx cannot reach PHP-FPM
This 502 shows up right after an upgrade or when someone hand-edits the FPM pool. The error log is explicit:
connect() to unix:/run/php/php8.3-fpm.sock failed (13: Permission denied)
Nginx workers run as www-data and the socket file must be reachable and writable by them. Check the pool config:
grep -E "^(listen\.owner|listen\.group|listen\.mode|user|group)" /etc/php/8.3/fpm/pool.d/www.conf
The correct values for a stock Ubuntu install are:
user = www-data
group = www-data
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
If the pool was changed to run as a deploy user, make sure listen.group = www-data (or add the deploy user to the www-data group) and restart FPM. Then confirm the socket ownership:
ls -l /run/php/php8.3-fpm.sock
It should show www-data www-data. One more gotcha: if the app's code lives under a home directory with 700 permissions, Nginx cannot even read the files — that gives 403 rather than 502, but the pool misconfig gives the 502 above. Keep them distinct in your head.
Verify it worked
- curl the URL and confirm a 200, not a 502:
curl -s -o /dev/null -w "%{http_code}\n" https://yourdomain.com/ - Watch the error log while loading the page once more — silence is the goal:
sudo tail -n 20 /var/log/nginx/error.log - Hit the slow/cookie-heavy pages specifically (login, admin dashboards, report exports) — Causes 4 and 5 only bite on specific URLs.
- Check the backend is healthy one last time:
sudo systemctl is-active php8.3-fpmorpm2 status, whichever applies.
Common issues & fixes
1. The error log is empty but users still see 502. You are probably reading the wrong log. Sites with a custom error_log directive write elsewhere — grep -rn error_log /etc/nginx/sites-enabled/ to find it. Also confirm you are testing the same server block users hit (right domain, right HTTPS port).
2. 502 only during deploys. Your restart kills the app before Nginx drains connections. For zero-downtime restarts, use a reload-style strategy: PM2 cluster pm2 reload, systemd with a graceful ExecReload, or two upstreams with max_fails/fail_timeout so Nginx routes around the restarting one.
3. 502 only on one specific page. Almost always Cause 4 (slow query on that page) or Cause 5 (huge headers on that page — login/SSO callbacks are the usual suspects). Time that page against the backend directly to see which.
4. I fixed everything and it still 502s. Did you actually reload? nginx -t only validates; systemctl reload nginx applies. Also check you edited the file in sites-enabled (or that sites-enabled symlinks are intact), not a stale copy in sites-available.
5. 502 started after a Cloudflare change. Cloudflare's own 502 page means Cloudflare could not reach your origin — the problem is still your server (causes above), or Cloudflare is talking to the wrong origin IP/port. Bypass Cloudflare with a direct hosts-file entry to your origin IP to confirm.
A 502 is Nginx telling you exactly where to look: the backend. Read the error log, match the line to the cause, apply the fix, and verify with curl. If you manage a fleet of servers and would rather not debug these at 2 a.m., XpertBees handles server setup, hardening, and monitoring for clients — reach out through the contact page.



