Skip to content
ohsudo
Networking

Nginx "502 Bad Gateway": find the cause in the error log

Fix nginx 502 Bad Gateway: read error.log, then fix the backend that is down, the wrong address or socket, the crashing app, or SELinux on RHEL.

Intermediate5 min readApplies to nginx · Ubuntu · Debian · RHEL / Rocky / AlmaLinux

by OhSudo ·

Your site opens, but the API, the app, or the whole page answers with this:

502 Bad Gateway
nginx

nginx is a gateway here: it passes requests to another program — the upstream or backend (Gunicorn, Node.js, PHP-FPM, a container…) — and returns its answer. A 502 means nginx is fine, but it didn’t get a valid answer from the backend. The page never says why. nginx’s error log does, and its message points to the exact cause.

TL;DR

Terminal window
sudo tail -n 20 /var/log/nginx/error.log # the real reason is here
grep -rn proxy_pass /etc/nginx/ # where does nginx send requests?
sudo ss -tlnp | grep 8000 # is anything listening there?
sudo systemctl status myapp # is the backend running?
sudo journalctl -u myapp -n 50 # why did it stop?

1. Read the error log

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

If your site has its own error_log line in the configuration, look in that file instead (sudo nginx -T | grep error_log shows them all). Every 502 leaves a line like this one:

2026/11/01 10:12:31 [error] 912#912: *57 connect() to 127.0.0.1:8000 failed (111: Connection refused) while connecting to upstream, client: 203.0.113.7, server: _, request: "GET /api/health HTTP/1.1", upstream: "http://127.0.0.1:8000/api/health", host: "example.com"

Two parts matter: the reason in parentheses and the upstream: address. Find your message below.

Message in error.log Section
(111: Connection refused) while connecting to upstream 2
(2: No such file or directory) on a unix: socket 3
upstream prematurely closed connection 4
[crit] … (13: Permission denied) while connecting to upstream 5
upstream sent too big header 6

2. Connection refused: the backend is not listening

connect() to 127.0.0.1:8000 failed (111: Connection refused) while connecting to upstream

nginx knocked on 127.0.0.1:8000 and nobody answered. Check, in this order:

Is the backend running?

Terminal window
sudo systemctl status webapp
× webapp.service - Gunicorn instance serving webapp
Active: failed (Result: oom-kill) since Sun 2026-11-01 10:12:02 UTC; 15min ago

If it’s stopped or failed, the journal tells you why — before you restart it:

Terminal window
sudo journalctl -u webapp -n 50 --no-pager

Result: oom-kill and killed by the OOM killer mean the system ran out of memory and the kernel killed the app. Start it again to bring the site back, then fix the cause, or it will happen again: fewer workers (--workers for Gunicorn), a memory leak in the app, or a server that needs more RAM.

Terminal window
sudo systemctl start webapp
curl -s -o /dev/null -w "%{http_code}\n" http://localhost/api/health

To have systemd restart the app on its own after a crash, add a drop-in with sudo systemctl edit webapp:

[Service]
Restart=on-failure
RestartSec=5

That keeps the site up, but it hides the problem — keep an eye on the journal.

Is it listening on the address nginx uses? Compare proxy_pass with what’s really open:

Terminal window
grep -rn proxy_pass /etc/nginx/
sudo ss -tlnp | grep 8000

Typical mismatches: the app listens on port 8080 but nginx sends to 8000; or the app listens on 127.0.0.1, and nginx sends to the server’s own public IP. Write the address exactly as ss shows it — proxy_pass http://127.0.0.1:8000;.

If proxy_pass uses localhost, nginx tries both [::1] and 127.0.0.1. An app that listens only on IPv4 still works, but the log fills with connect() to [::1]:8000 failed lines — writing 127.0.0.1 removes that noise.

Note

nginx in Docker: inside a container, 127.0.0.1 is the container itself, not your server. Use the other container’s service name instead (proxy_pass http://app:8000; with Docker Compose), and make sure both are on the same network.

3. Wrong Unix socket (PHP-FPM, Gunicorn)

connect() to unix:/run/php/php8.1-fpm.sock failed (2: No such file or directory) while connecting to upstream

nginx looks for a socket file that doesn’t exist. With PHP this happens almost always after an upgrade: the PHP version is part of the socket name, and the nginx config still has the old one. See which sockets really exist:

Terminal window
ls -l /run/php/ # Debian / Ubuntu
ls -l /run/php-fpm/ # RHEL / Rocky / AlmaLinux

Put the right path in fastcgi_pass (or proxy_pass for Gunicorn), check that the service is running (systemctl status php8.3-fpm / php-fpm), then sudo nginx -t && sudo systemctl reload nginx.

4. The backend crashes while it answers

upstream prematurely closed connection while reading response header from upstream

The connection worked, but the backend closed it before it sent an answer: the process died in the middle of the request. Look at the backend’s own log at the same moment. Usual causes:

  • The app crashed on this specific request (an unhandled exception — the app’s log has the stack trace).
  • A worker was killed for being too slow: Gunicorn writes [CRITICAL] WORKER TIMEOUT after 30 seconds by default. Make the slow request faster, or raise --timeout.
  • Out of memory — sudo journalctl -k | grep -i "out of memory" shows the kernel’s OOM kills.

5. Permission denied: SELinux or socket permissions

[crit] 912#912: *57 connect() to 127.0.0.1:8000 failed (13: Permission denied) while connecting to upstream

On RHEL / Rocky / AlmaLinux with a TCP upstream, this is SELinux. By default, nginx (which runs under the httpd_t label) is not allowed to open network connections to other programs. The backend works, curl http://127.0.0.1:8000 works — only nginx is blocked. Check and allow it:

Terminal window
getsebool httpd_can_network_connect
sudo setsebool -P httpd_can_network_connect 1

-P makes the change permanent. No restart is needed. The audit log confirms the cause: sudo ausearch -m avc -ts recent | grep nginx.

Warning

Don’t turn SELinux off to fix this. setenforce 0 “works”, but it removes protection from the whole server — the boolean allows only what nginx needs.

With a Unix socket (on any distribution), error 13 means the nginx user (www-data on Debian/Ubuntu, nginx on RHEL) can’t open the socket file. Check ls -l on the socket, and set the owner in the backend’s config — for PHP-FPM, listen.owner and listen.group in the pool file.

6. The response headers are too big

upstream sent too big header while reading response header from upstream

The backend answered, but its headers — usually large cookies or a long redirect URL — don’t fit in nginx’s buffer. Make the buffers bigger in the location that proxies to the app:

proxy_buffer_size 16k;
proxy_buffers 8 16k;

For PHP-FPM, the equivalents are fastcgi_buffer_size and fastcgi_buffers. Then sudo nginx -t && sudo systemctl reload nginx.

Shell, we have a problem!

Similar, but different: 504 Gateway Time-out means the backend accepted the request but took too long to answer (upstream timed out (110: Connection timed out) in error.log). Find out why the request is slow, or raise proxy_read_timeout, which is 60 seconds by default. 503 Service Unavailable usually comes from nginx’s own rate limiting, or from the backend itself.

$ ./practice.sh

Practice this in the terminal

Open the Terminal Playground with this article’s scenario loaded. It is safe and runs in your browser.

Open the terminal →

Found a mistake? Let us know at contact@ohsudo.com.

Tools

Try it yourself

Articles

Related articles