Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for NGINX By James Joyner IV · · 10 min read

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, or no live upstreams.
  • The site works when you curl the 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_pass from it, so they can never drift apart.
  • Always pair upstream keepalive with proxy_http_version 1.1; and proxy_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_timeout and alert on connect() failed and upstream prematurely closed lines 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:

  1. The backend process (PHP-FPM, Gunicorn, Node, Java) is down or crashed.
  2. proxy_pass/fastcgi_pass targets the wrong host, port, or socket.
  3. The upstream closed the connection before sending a complete response.
  4. Every member of an upstream {} block was ejected (no live upstreams).
  5. SELinux denies NGINX the outbound connection (13: Permission denied).
  6. 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.

Free download · 368-page PDF

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.