Nginx 502 Bad Gateway — Upstream Connection Refused or Closed
Diagnose nginx 502 errors by reading the error log, testing the upstream directly, and checking listener addresses, timeouts, and proxy protocol settings.
Symptoms
- 502 Bad Gateway for some or all requests
- error.log shows connect() failed (111: Connection refused) while connecting to upstream
- error.log shows upstream prematurely closed connection while reading response header
- Static files load but API routes fail through the same nginx server
Possible causes
- The application is not running or crashed after nginx started
- The app listens on 127.0.0.1 while nginx connects to a container or pod IP, or the reverse
- The upstream port in the proxy_pass directive is stale after a deployment
- The app closes the connection early because of a timeout, crash, or protocol mismatch
Diagnosis steps
Read the exact upstream error
tail -n 100 /var/log/nginx/error.log | grep -E 'upstream|connect|502'Interpretation
Connection refused points to no listener at the address and port. Prematurely closed connection points to the app accepting then aborting. Timeout points to a slow app.
Print the effective proxy configuration
nginx -T | grep -n -A8 -E 'upstream|proxy_pass'Interpretation
Confirm the host and port nginx actually uses, including inherited server blocks. A different port than the app listens on explains a 502 that survives restarts.
Test the upstream from the nginx host
curl -v --max-time 5 http://127.0.0.1:3000/health && ss -ltnp | grep 3000Interpretation
A successful curl proves the app works locally. If curl fails, nginx is not the root cause; if curl works but nginx still fails, check addresses and namespaces.
Check whether the app listens on all interfaces
ss -ltnp | grep -E ':(3000|8080)' && getenforce 2>/dev/nullInterpretation
A listener bound to 127.0.0.1 is unreachable from another container. Enforcing SELinux can also block nginx from making outbound connections until httpd_can_network_connect is enabled.
Root cause
nginx returns 502 when it cannot obtain a valid response from the upstream selected by proxy_pass. The TCP connection can be refused because nothing listens at that address, or accepted and then reset by an application that crashes, rejects the request, or exceeds the proxy timeout. In container deployments the mismatch is usually an address or port that is correct inside the app but not from the nginx network namespace.
Fix
Start or stabilize the application, point proxy_pass at a reachable address and port, and make the app listen on 0.0.0.0 inside its container. Keep proxy_connect_timeout, proxy_read_timeout, and proxy_send_timeout consistent with the app's real latency. For early aborts, inspect app logs and confirm the request line, headers, and body match what the framework expects.
Risk note
Raising proxy_read_timeout hides slow endpoints instead of fixing them. Using a load balancer or resolver without health checks can keep sending traffic to a dead upstream. Disabling SELinux enforcement broadens the attack surface and should not be the permanent fix.
Verify the fix
Request the failing route through nginx with curl -i and confirm a 200 or the expected status, while the error log stays free of upstream messages. Repeat during a rolling restart to ensure at least one healthy upstream remains.
502 is an upstream conversation failure
nginx returns 502 when it selected a backend but could not complete a valid HTTP exchange. The error log distinguishes the two common shapes:
connect() failed (111: Connection refused) while connecting to upstream
upstream prematurely closed connection while reading response header from upstream
The first means nothing accepted the TCP connection. The second means the app accepted it and then closed before sending a complete response.
Reproduce outside nginx
nginx -T | grep -n -A8 -E 'upstream|proxy_pass'
curl -v --max-time 5 http://127.0.0.1:3000/health
ss -ltnp | grep 3000
If curl succeeds from the nginx host but the proxy fails, compare the configured address with what the app actually binds. A process listening on 127.0.0.1:3000 is invisible to nginx running in another container, and localhost inside a container points at the container itself.
Fix the route
upstream api {
server 10.0.3.17:3000 max_fails=3 fail_timeout=10s;
keepalive 16;
}
server {
location /api/ {
proxy_pass http://api/;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_connect_timeout 3s;
proxy_read_timeout 30s;
}
}
Make the app listen on 0.0.0.0, verify the port after every deploy, and check SELinux with getenforce and ausearch -m avc -ts recent if the connection is blocked despite a listening socket.
Verify
curl -i https://example.com/api/health
tail -f /var/log/nginx/error.log
Confirm the response is the application’s own status and body, then trigger a rolling restart and ensure requests continue through a healthy upstream. A 502 that only appears under load usually means the upstream is crashing or timing out, so correlate with application logs rather than raising timeouts first.