NGINX Error Guide: '502 Bad Gateway' from a Failing Upstream
Fix NGINX 502 Bad Gateway errors: diagnose dead upstreams, wrong proxy_pass ports, crashed PHP-FPM/app servers, SELinux socket blocks, and bad keepalive settings.
- #nginx
- #troubleshooting
- #errors
- #proxy
Overview
A 502 Bad Gateway means NGINX, acting as a reverse proxy, received an invalid or empty response from the upstream server it forwarded the request to. NGINX itself is healthy — the application behind it (PHP-FPM, a Node/Gunicorn/Java process, or another HTTP server) refused the connection, crashed mid-response, or returned garbage that NGINX could not parse as a valid HTTP reply.
The browser sees a generic page, but the truth is in the NGINX error log:
2026/06/23 14:02:11 [error] 2841#2841: *10532 connect() failed (111: Connection refused) while connecting to upstream, client: 203.0.113.7, server: app.example.com, request: "GET /api/health HTTP/1.1", upstream: "http://127.0.0.1:9000/api/health", host: "app.example.com"
The status code is fixed at 502; the cause varies. It is almost always a problem with the process behind proxy_pass (or fastcgi_pass), not with NGINX.
Symptoms
- Browser shows “502 Bad Gateway” and the default NGINX error page.
- error.log records
connect() failed,upstream prematurely closed connection, orno live upstreams. - The site works when you
curlthe backend directly but not through NGINX. - Intermittent 502s under load while the backend is being restarted or is overloaded.
curl -sI https://app.example.com/api/health
HTTP/1.1 502 Bad Gateway
Server: nginx/1.24.0
Content-Type: text/html
sudo tail -20 /var/log/nginx/error.log
[error] 2841#2841: *10532 connect() failed (111: Connection refused) while connecting to upstream, upstream: "http://127.0.0.1:9000/"
Common Root Causes
1. The upstream process is down or crashed
The most frequent cause: the app server NGINX proxies to is not running, so the connection is refused.
sudo systemctl status php8.2-fpm
sudo ss -ltnp | grep -E ':9000|php-fpm'
[error] 2841#2841: *10532 connect() failed (111: Connection refused) while connecting to upstream, upstream: "http://127.0.0.1:9000/"
If ss shows nothing listening on the upstream port/socket, the backend died. Restart it and the 502 clears.
2. proxy_pass points at the wrong host or port
A typo or stale config sends traffic to a port nothing listens on.
grep -RnE 'proxy_pass|fastcgi_pass' /etc/nginx/conf.d/ /etc/nginx/sites-enabled/
location /api/ {
proxy_pass http://127.0.0.1:8080; # app actually listens on 3000
}
[error] connect() failed (111: Connection refused) while connecting to upstream, upstream: "http://127.0.0.1:8080/api/"
The backend listens on 3000; NGINX dials 8080 and gets refused.
3. Upstream closed the connection prematurely
The backend accepted the connection then died or timed out before sending a complete response — common with PHP-FPM fatal errors or an OOM-killed worker.
sudo tail -50 /var/log/php8.2-fpm.log
[error] 2841#2841: *10544 upstream prematurely closed connection while reading response header from upstream, upstream: "fastcgi://unix:/run/php/php8.2-fpm.sock:", request: "POST /upload.php"
The PHP child exited (segfault, memory_limit, or max_execution_time) before flushing headers.
4. All servers in the upstream block are marked down
With an upstream {} block, NGINX takes failed backends out of rotation; if every member fails its health checks you get no live upstreams.
grep -A6 'upstream ' /etc/nginx/conf.d/app.conf
[error] 2841#2841: *10590 no live upstreams while connecting to upstream, upstream: "http://backend/", request: "GET / HTTP/1.1"
max_fails/fail_timeout ejected all members. Confirm each member is actually reachable before blaming NGINX.
5. SELinux blocks NGINX from connecting to the backend socket
On RHEL/Rocky/Alma, SELinux denies NGINX outbound network connections unless httpd_can_network_connect is on. The connection is refused even though the backend is up.
sudo ausearch -m avc -ts recent | grep nginx
sudo getsebool httpd_can_network_connect
type=AVC msg=audit(...): avc: denied { name_connect } for pid=2841 comm="nginx" dest=9000 scontext=system_u:system_r:httpd_t:s0 ...
httpd_can_network_connect --> off
[crit] 2841#2841: *10610 connect() to 127.0.0.1:9000 failed (13: Permission denied) while connecting to upstream
Note 13: Permission denied (not 111) — that is the SELinux signature.
6. Stale keepalive connections to the upstream
When keepalive is enabled in the upstream block but proxy_http_version 1.1; / clearing the Connection header is missing, NGINX reuses connections the backend has already closed.
grep -RnE 'keepalive|proxy_http_version|Connection' /etc/nginx/conf.d/app.conf
[error] 2841#2841: *10700 upstream prematurely closed connection while reading response header from upstream, upstream: "http://127.0.0.1:3000/"
Add proxy_http_version 1.1; and proxy_set_header Connection ""; in the location to stop reusing dead sockets.
Diagnostic Workflow
Step 1: Confirm it is really a 502 and capture the upstream
curl -sI https://app.example.com/ | head -1
sudo tail -20 /var/log/nginx/error.log
The error line names the exact upstream: URL/socket and the OS error (111 refused, 13 permission, “prematurely closed”). That single line points to the cause.
Step 2: Test the backend directly, bypassing NGINX
curl -sv http://127.0.0.1:9000/ # or the port/socket from proxy_pass
sudo ss -ltnp | grep -E ':9000|:3000|php-fpm'
If curl to the backend also fails, the problem is the app, not NGINX. If ss shows nothing listening, the process is down.
Step 3: Check the backend’s own logs and service state
sudo systemctl status php8.2-fpm app-gunicorn
sudo tail -50 /var/log/php8.2-fpm.log
sudo journalctl -u app-gunicorn --no-pager | tail -50
Look for fatal errors, OOM kills, or repeated restarts that line up with the 502 timestamps.
Step 4: Validate the NGINX config matches the backend
grep -RnE 'proxy_pass|fastcgi_pass|upstream' /etc/nginx/conf.d/ /etc/nginx/sites-enabled/
sudo nginx -t
Confirm the port/socket in proxy_pass is exactly where the backend listens, and that nginx -t reports the config is valid before reloading.
Step 5: Rule out SELinux, then reload
sudo ausearch -m avc -ts recent | grep nginx
sudo getsebool httpd_can_network_connect
If a name_connect AVC is present, allow it and reload:
sudo setsebool -P httpd_can_network_connect on
sudo nginx -t && sudo systemctl reload nginx
Example Root Cause Analysis
After a deploy, app.example.com returns 502 on every request. The browser shows the NGINX error page; NGINX itself is up.
The error log names the upstream:
[error] 2841#2841: *10532 connect() failed (111: Connection refused) while connecting to upstream, upstream: "http://127.0.0.1:8000/", request: "GET / HTTP/1.1"
Testing the backend directly:
curl -sv http://127.0.0.1:8000/
sudo ss -ltnp | grep gunicorn
* connect to 127.0.0.1 port 8000 failed: Connection refused
LISTEN 0 2048 127.0.0.1:8001 ... users:(("gunicorn",pid=4120,...))
Gunicorn now listens on 8001 — the new deploy changed its bind address — but proxy_pass still targets 8000. NGINX dials a port nothing listens on and gets refused.
Fix: align proxy_pass with the backend’s actual port, validate, and reload:
# change proxy_pass http://127.0.0.1:8000; -> :8001 in app.conf, then:
sudo nginx -t
sudo systemctl reload nginx
The next request reaches Gunicorn and returns 200.
Prevention Best Practices
- Run a health-check probe (
curl -f http://127.0.0.1:<port>/healthz) in your deploy pipeline so a backend that fails to bind blocks the release instead of surfacing as user-facing 502s. - Pin the backend’s listen port/socket in one place and template both the app and the NGINX
proxy_passfrom it, so they can never drift apart. - Always pair upstream
keepalivewithproxy_http_version 1.1;andproxy_set_header Connection "";to avoid reusing closed sockets. - On RHEL-family hosts, set
httpd_can_network_connect(or the right socket label) as part of provisioning so SELinux never silently refuses the proxy. - Set conservative
proxy_connect_timeout/proxy_read_timeoutand alert onconnect() failedandupstream prematurely closedlines in error.log. - For fast triage of a 502 storm, the free incident assistant can read the upstream error lines and point at the likely backend. More NGINX fixes live in the NGINX guides.
Quick Command Reference
# Confirm the 502 and read the upstream error
curl -sI https://app.example.com/ | head -1
sudo tail -20 /var/log/nginx/error.log
# Is the backend actually listening?
sudo ss -ltnp | grep -E ':9000|:3000|:8000|php-fpm'
curl -sv http://127.0.0.1:9000/
# Check backend service + logs
sudo systemctl status php8.2-fpm
sudo tail -50 /var/log/php8.2-fpm.log
# Confirm proxy_pass matches the backend
grep -RnE 'proxy_pass|fastcgi_pass|upstream' /etc/nginx/conf.d/ /etc/nginx/sites-enabled/
# Rule out SELinux (RHEL family)
sudo ausearch -m avc -ts recent | grep nginx
sudo setsebool -P httpd_can_network_connect on
# Validate and reload
sudo nginx -t && sudo systemctl reload nginx
Conclusion
A 502 Bad Gateway is NGINX telling you the upstream gave it nothing usable. The usual root causes:
- The backend process (PHP-FPM, Gunicorn, Node, Java) is down or crashed.
proxy_pass/fastcgi_passtargets the wrong host, port, or socket.- The upstream closed the connection before sending a complete response.
- Every member of an
upstream {}block was ejected (no live upstreams). - SELinux denies NGINX the outbound connection (
13: Permission denied). - Stale keepalive connections reused against a backend that already closed them.
Read the upstream: line in error.log first — it names the exact target and OS error, and the fix is almost always restarting the backend or aligning proxy_pass with where it actually listens.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.