Nginx 502 Bad Gateway: Diagnose and Fix It in Minutes


Your site is returning 502 Bad Gateway and nginx’s error log says something like:

*1 connect() failed (111: Connection refused) while connecting to upstream

A 502 almost never means nginx is broken. It means nginx tried to hand the request to something behind it — PHP-FPM, a Node process, Gunicorn, another server — and got nothing back. Your job is to find out which of those two ends stopped talking. That takes about two minutes if you read the log first.

Key Takeaways

  • A 502 means nginx reached your upstream address but got no usable response — the upstream is down, wrong, or too slow.
  • The nginx error log names the exact cause; error 111 means connection refused, 110 means timeout, and each points at a different fix.
  • Most cases are an upstream service that crashed or a proxy_pass address pointing at the wrong port or socket path.
  • Timeout-flavoured 502s need proxy_read_timeout raised only after you confirm the upstream is genuinely slow, not stuck.
  • SELinux on RHEL-family systems silently blocks nginx from connecting to upstreams until httpd_can_network_connect is enabled.

Quick Fix

Read the last lines of the nginx error log to see which upstream failed and why, then confirm that upstream is actually listening on the address in your proxy_pass or fastcgi_pass directive.

sudo tail -n 20 /var/log/nginx/error.log

Read the Error Log Before Changing Anything

Nginx tells you exactly what happened. Every 502 writes a line to the error log naming the upstream it tried and the reason it failed.

sudo tail -n 20 /var/log/nginx/error.log

You are looking for the number in parentheses:

  • (111: Connection refused) — nothing is listening at that address. The upstream is stopped, or you have the wrong port or socket path.
  • (110: Connection timed out) — something is listening but never replied in time. The upstream is overloaded or deadlocked.
  • (2: No such file or directory) — the Unix socket in your config does not exist. Usually a PHP-FPM version mismatch.
  • (13: Permission denied) — the socket exists but nginx cannot open it. Ownership or SELinux.

The rest of this guide is ordered by how often each cause turns up. Start at the top.

Cause 1: The Upstream Service Is Not Running

This is the cause roughly two-thirds of the time. Check whether the service behind nginx is alive:

systemctl status php8.3-fpm    # or your app: gunicorn, node, puma

If the output says inactive (dead) or failed, that is your 502. Start it and watch what happens:

sudo systemctl start php8.3-fpm
systemctl status php8.3-fpm

If it starts and stays running, reload your site — the 502 should be gone. If it immediately fails again, the service is crashing on startup and the real error is in its own journal:

sudo journalctl -u php8.3-fpm -n 50 --no-pager

Common culprits there are a syntax error in a recently edited config file, a port already taken by another process, or a PHP extension that was removed during an upgrade.

Cause 2: proxy_pass Points at the Wrong Address

If the service is running, the next question is whether nginx is knocking on the right door. Find the address nginx is using:

grep -rE "proxy_pass|fastcgi_pass" /etc/nginx/sites-enabled/ /etc/nginx/conf.d/

Now confirm something is actually listening there. For a TCP port:

sudo ss -ltnp | grep 3000

For a Unix socket, check the file exists:

ls -l /run/php/php8.3-fpm.sock

An empty result from either command means nginx is pointing somewhere nothing lives. Two mismatches account for most of these:

PHP version drift after an upgrade. You upgrade PHP from 8.1 to 8.3, the old socket disappears, but your nginx config still says /run/php/php8.1-fpm.sock. Update the path to match the version actually installed, then reload.

App bound to the wrong interface. Your Node or Gunicorn app is listening on 127.0.0.1:3000 but nginx is proxying to localhost:3000 on a system where localhost resolves to IPv6 ::1 first. Use the explicit IPv4 address in proxy_pass to remove the ambiguity.

After any config edit, always test before reloading — nginx will refuse to reload a broken config, but testing first tells you why:

sudo nginx -t && sudo systemctl reload nginx

Cause 3: The Upstream Is Too Slow (Timeout 502s)

If your log shows 110: Connection timed out or upstream timed out, the service is alive but did not answer within nginx’s default 60 seconds. Before raising the timeout, find out whether the request is genuinely long-running or simply stuck.

Watch what the upstream is doing while you reproduce the request:

sudo journalctl -u your-app -f

If the app logs the request start and then nothing, it is hanging — usually on a database query or an external API call with no timeout of its own. Raising nginx’s timeout hides that instead of fixing it.

If the work is legitimately slow (a large report, a video transcode), raise the timeout for that location only, not globally:

location /export/ {
    proxy_pass http://127.0.0.1:3000;
    proxy_read_timeout 300s;
    proxy_connect_timeout 10s;
}

Reload nginx and retest. Keep the raised value scoped to the slow endpoint so a genuinely hung request elsewhere still fails fast instead of tying up a worker for five minutes.

Cause 4: Permissions and SELinux

A 13: Permission denied on a Unix socket means the socket exists but nginx cannot open it. Check who owns it:

ls -l /run/php/php8.3-fpm.sock

The socket’s group should be one nginx belongs to — usually www-data on Debian and Ubuntu. If your PHP-FPM pool config sets listen.owner and listen.group to something else, align them with the nginx user and restart PHP-FPM.

On RHEL, Rocky, AlmaLinux, and Fedora, SELinux blocks nginx from making network connections to upstreams by default. This produces a 502 with no obvious cause in the nginx log. Check whether SELinux is the culprit:

sudo grep nginx /var/log/audit/audit.log | grep denied | tail -5

If you see denials, allow the connection:

sudo setsebool -P httpd_can_network_connect 1

The -P makes it survive reboots. This single setting resolves a large share of 502s on freshly provisioned RHEL-family servers.

Verify the Fix

Confirm the fix from the server itself, which rules out DNS, CDN, and browser caching muddying the result:

curl -I https://localhost/ --resolve localhost:443:127.0.0.1 -k

You want HTTP/1.1 200 OK. If you get a 502 here but your browser shows something else, the difference is a caching layer in front of nginx, not nginx itself.

Then watch the error log while you load a few real pages. Silence is success:

sudo tail -f /var/log/nginx/error.log

Prevent It From Happening Again

Most repeat 502s come from an upstream that dies and never gets restarted. Make systemd bring it back automatically. Create an override rather than editing the packaged unit file, which upgrades would overwrite:

sudo systemctl edit php8.3-fpm

Add:

[Service]
Restart=always
RestartSec=5

Save, then reload systemd so the override takes effect:

sudo systemctl daemon-reload

Two habits close the loop. Run sudo nginx -t before every reload so a typo never takes the site down. And after any PHP or runtime upgrade, grep your nginx configs for the old version string — stale socket paths are the single most common self-inflicted 502.

Frequently Asked Questions

What is the difference between a 502 and a 504 error?

A 502 Bad Gateway means nginx got an invalid or empty response from the upstream, often because nothing was listening. A 504 Gateway Timeout means nginx connected successfully but the upstream did not finish replying within the configured timeout.

Why does nginx return 502 only under heavy traffic?

Your upstream is running out of workers. For PHP-FPM, raise pm.max_children in the pool config; for Node or Gunicorn, increase worker count. Check the upstream’s own log for messages about reaching the process limit before changing nginx settings.

Does restarting nginx fix a 502?

Rarely, because the fault is usually in the upstream rather than nginx. Restarting the upstream service fixes it far more often. If restarting nginx does help, the underlying cause is likely exhausted worker connections, which will recur.

How do I find which upstream is failing when I proxy to several?

The nginx error log prints the failing upstream address on every 502 line, for example ‘while connecting to upstream, upstream: http://127.0.0.1:3000/’. Match that address against your proxy_pass directives to identify the service.

Scroll to Top