Uptime monitoring from outside your own infrastructure that alerts you the minute your site goes down — upordown.online
A 502 Bad Gateway from Nginx is one of the least informative pages on the internet. The browser shows a grey "502 Bad Gateway / nginx" and nothing else. The good news is that Nginx knows exactly why it returned that page, and it writes the reason to its error log every single time.
So the whole fix comes down to two steps: find the right log line, then match it to a cause. This post covers both, plus the handful of commands that confirm the diagnosis before you change anything.
What 502 actually means
Nginx is acting as a reverse proxy. It accepted the visitor's request, tried to pass it to an upstream (Node, Gunicorn, PHP-FPM, a container, another server), and the upstream either refused the connection, hung up halfway, or sent back something Nginx couldn't parse as HTTP.
That's different from a 504, where the upstream accepted the request but took too long. If your log says upstream timed out, you're looking at a timeout problem, not a 502 problem, and the fix is different.
Step 1: read the error log, not the access log
The access log only tells you that a 502 happened. The error log tells you why.
# Default locations on Debian/Ubuntu and most RHEL-family installs sudo tail -f /var/log/nginx/error.log # If you're not sure where it is sudo nginx -T 2>/dev/null | grep -E '^\s*error_log' # Docker docker logs -f <nginx-container> 2>&1 | grep -i upstream Reload the failing page while the tail is running. You'll get a line like this:
2026/11/14 09:12:44 [error] 1234#1234: *5678 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/", host: "example.com" Two parts matter: the message after [error], and the upstream: address. The address tells you exactly where Nginx tried to go, which catches a surprising number of typos on its own.
Need someone else to catch the next 502? upordown.online watches your site from outside and alerts you the moment Nginx starts returning errors.
Step 2: match the line to a cause
connect() failed (111: Connection refused) while connecting to upstream
Nothing is listening on that address and port. The app crashed, didn't start, or is listening somewhere else.
ss -ltnp | grep 3000 # is anything listening on the port? systemctl status myapp # did the service die? journalctl -u myapp -n 50 # why did it die? curl -i http://127.0.0.1:3000/ # does the app answer without Nginx? A classic version: the app listens on localhost:3000, which resolves to ::1 (IPv6), while Nginx connects to 127.0.0.1 (IPv4). Make both sides use the same explicit address.
connect() to unix:/run/php/php8.3-fpm.sock failed (2: No such file or directory)
The socket path in fastcgi_pass or proxy_pass doesn't exist. Usually PHP-FPM was upgraded and the socket is now php8.4-fpm.sock, or the service isn't running. ls -l /run/php/ shows what's actually there.
... failed (13: Permission denied) while connecting to upstream
Two common causes. With a Unix socket, the Nginx worker user (www-data or nginx) can't read or write the socket file: fix listen.owner, listen.group and listen.mode in the FPM pool config. On RHEL, Fedora or CentOS with SELinux enforcing, Nginx isn't allowed to open network connections to upstreams at all until you run:
sudo setsebool -P httpd_can_network_connect 1 upstream prematurely closed connection while reading response header from upstream
Nginx connected, sent the request, and the upstream hung up before sending a response. Usually the app crashed while handling that request (check its logs for a stack trace at the same timestamp), or a worker was killed for using too much memory (dmesg | grep -i oom), or Gunicorn's worker timeout killed a slow request.
If it only happens on some requests under load, look at keepalive: an upstream that closes idle keepalive connections sooner than Nginx expects will produce occasional 502s. Setting the app's keepalive timeout slightly higher than Nginx's keepalive_timeout in the upstream block removes the race.
upstream sent too big header while reading response header from upstream
The app's response headers are bigger than Nginx's buffer, usually because of large cookies or a long Set-Cookie chain after login. Raise the buffers:
proxy_buffer_size 16k; proxy_buffers 8 16k; proxy_busy_buffers_size 32k; # for PHP-FPM, the fastcgi_ equivalents Then ask why the headers are that big. A 12 KB session cookie is a bug of its own.
no live upstreams while connecting to upstream
You have an upstream block with several servers, and Nginx has marked all of them as failed (passive health checks via max_fails and fail_timeout). The real error is earlier in the log: scroll up to find what made each server fail the first time.
SSL_do_handshake() failed ... while SSL handshaking to upstream
You're proxying to an HTTPS upstream, and the TLS handshake fails. Very often the upstream needs SNI, which Nginx doesn't send by default:
proxy_pass https://backend.example.net; proxy_ssl_server_name on; Step 3: confirm before you change anything
Whatever the log says, reproduce the problem without Nginx in the way. If curl -i http://127.0.0.1:3000/health from the Nginx host also fails, Nginx is the messenger and the fix is in the app. If curl works and Nginx still returns 502, compare the address in the log's upstream: field with what you just curled. They're different more often than you'd expect.
After any config change:
sudo nginx -t && sudo systemctl reload nginx nginx -t catches syntax errors before a reload turns one 502 into a site-wide outage.
Making the next 502 shorter
- Log upstream details in the access log. Adding
$upstream_addr $upstream_status $upstream_response_timeto yourlog_formatshows which backend failed and how long it took, per request. - Give the app a health endpoint that doesn't depend on Nginx, so you can curl it directly.
- Check from outside. A 502 that only appears for some regions or some backends is easy to miss from your own laptop. An external check like upordown.online sees what visitors see, which is the version of the error that matters.
Related reading
- 503 vs 504: what each error means and who has to fix it
- How to read curl -v output to find where a request fails
- ECONNREFUSED vs ETIMEDOUT vs ECONNRESET: what your monitor's error is actually telling you
- Is it down, or is it me? Debugging a site that won't load, one layer at a time
- Adding rate limiting to your API? A 12-point checklist so it doesn't block your own users (or your monitor)
Which 502 log line have you seen most often? And did anyone else lose an afternoon to the localhost resolving to IPv6 one?