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

NGINX Error Guide: 'connect() failed (111: Connection refused) while connecting to upstream'

Fix NGINX connect() failed (111: Connection refused) to upstream: diagnose a down backend, wrong proxy_pass port, localhost vs socket mismatch, firewall, and SELinux.

  • #nginx
  • #troubleshooting
  • #errors
  • #upstream

Overview

connect() failed (111: Connection refused) while connecting to upstream means NGINX tried to open a TCP connection to the backend defined in proxy_pass/fastcgi_pass and the OS immediately returned ECONNREFUSED (errno 111). A “connection refused” specifically means nothing is listening on that address and port — the kernel sent a TCP RST. This surfaces to the client as a 502 Bad Gateway, but the error log pinpoints the cause.

The signature line:

2026/06/23 17:22:40 [error] 6120#6120: *40711 connect() failed (111: Connection refused) while connecting to upstream, client: 203.0.113.9, server: api.example.com, request: "GET /v1/users HTTP/1.1", upstream: "http://127.0.0.1:8000/v1/users", host: "api.example.com"

111: Connection refused is distinct from 110: Connection timed out (firewall/DROP) and 13: Permission denied (SELinux). Refused = the backend port is closed because the process is down or NGINX is dialing the wrong place.

Symptoms

  • Clients get 502; error.log shows connect() failed (111: Connection refused).
  • ss/curl against the upstream port returns “Connection refused”.
  • The backend works on a different port/socket than proxy_pass targets.
  • Started happening right after a backend restart, redeploy, or port change.
sudo grep "Connection refused" /var/log/nginx/error.log | tail -5
[error] 6120#6120: *40711 connect() failed (111: Connection refused) while connecting to upstream, upstream: "http://127.0.0.1:8000/v1/users"
curl -sv http://127.0.0.1:8000/v1/users 2>&1 | grep -i refused
* connect to 127.0.0.1 port 8000 failed: Connection refused

Common Root Causes

1. The backend process is not running

The simplest cause: nothing is listening on the upstream port because the app crashed or was never started.

sudo systemctl status app-gunicorn
sudo ss -ltnp '( sport = :8000 )'
● app-gunicorn.service - App
   Active: inactive (dead)
[error] 6120#6120: *40711 connect() failed (111: Connection refused) while connecting to upstream, upstream: "http://127.0.0.1:8000/"

The service is dead, the port is empty, every connect is refused. Start the backend.

2. proxy_pass points at the wrong port

The backend is up but on a different port than the config dials.

grep -RnE 'proxy_pass|fastcgi_pass' /etc/nginx/conf.d/ /etc/nginx/sites-enabled/
sudo ss -ltnp | grep gunicorn
proxy_pass http://127.0.0.1:8000;     # config
LISTEN 0 2048 127.0.0.1:8001 ... gunicorn   # actual
[error] connect() failed (111: Connection refused) while connecting to upstream, upstream: "http://127.0.0.1:8000/"

NGINX dials 8000; the app listens on 8001. Align them.

3. Backend bound to a different interface than NGINX dials

The app listens on 127.0.0.1 but NGINX proxies to localhost resolving to IPv6 ::1, or vice-versa — so the connect lands on an address nothing listens on.

grep -RnE 'proxy_pass' /etc/nginx/conf.d/app.conf
sudo ss -ltnp '( sport = :8000 )'
proxy_pass http://localhost:8000;        # may resolve to [::1]:8000
LISTEN 0 2048 127.0.0.1:8000 ... uvicorn # only IPv4 loopback
[error] connect() failed (111: Connection refused) while connecting to upstream, upstream: "http://[::1]:8000/"

localhost resolved to IPv6 ::1; the app only bound IPv4 127.0.0.1. Use the explicit IP in proxy_pass.

4. Unix socket path mismatch (PHP-FPM)

fastcgi_pass references a socket that does not exist (wrong PHP version path) or that FPM is not listening on.

grep -RnE 'fastcgi_pass' /etc/nginx/
ls -l /run/php/
fastcgi_pass unix:/run/php/php8.1-fpm.sock;   # config
-rw-rw---- 1 www-data www-data /run/php/php8.2-fpm.sock   # actual
[error] connect() failed (111: Connection refused) while connecting to upstream, upstream: "fastcgi://unix:/run/php/php8.1-fpm.sock:"

Config points at the 8.1 socket; only 8.2 exists. Update the socket path.

5. A firewall is dropping the loopback/backend connection

Less common for loopback, but a host firewall rule on a remote upstream IP can refuse the connect (a REJECT rule returns RST = “refused”).

sudo iptables -L -n | grep -E '8000|REJECT'
nc -vz 10.0.0.20 8000
REJECT     tcp  --  0.0.0.0/0  10.0.0.20  tcp dpt:8000 reject-with icmp-port-unreachable

A REJECT rule produces “connection refused”; a DROP rule would instead produce a timeout (110).

6. SELinux denies the connect (shows as 13, not 111)

On RHEL-family hosts, an SELinux denial returns 13: Permission denied rather than 111 — worth ruling in/out because the user-visible 502 looks identical.

sudo ausearch -m avc -ts recent | grep nginx
sudo getsebool httpd_can_network_connect
avc:  denied  { name_connect } for pid=6120 comm="nginx" dest=8000 scontext=system_u:system_r:httpd_t:s0
httpd_can_network_connect --> off

If you see 13: Permission denied (not 111), it is SELinux, not a closed port.

Diagnostic Workflow

Step 1: Read the exact upstream and errno

sudo grep -E "connect\(\) failed" /var/log/nginx/error.log | tail -5

Note the upstream: target and the errno: 111 (refused = nothing listening), 110 (timeout = firewall/DROP), 13 (SELinux). This decides which branch to follow.

Step 2: Probe the upstream directly

curl -sv http://127.0.0.1:8000/ 2>&1 | tail -5
sudo ss -ltnp '( sport = :8000 )'

If ss shows nothing on the port, the backend is down or on another port. If it shows a listener but on a different IP family, that is the mismatch.

Step 3: Confirm the backend service state

sudo systemctl status app-gunicorn php8.2-fpm
sudo journalctl -u app-gunicorn --no-pager | tail -30

Restart it if dead, and check the logs for a bind failure that explains why it never came up on the expected port.

Step 4: Compare proxy_pass against the real listener

grep -RnE 'proxy_pass|fastcgi_pass' /etc/nginx/conf.d/ /etc/nginx/sites-enabled/
ls -l /run/php/   # for socket-based backends

Make the proxy_pass/fastcgi_pass host, port, IP family, or socket path match exactly what ss/ls shows. Prefer 127.0.0.1 over localhost to avoid IPv6 surprises.

Step 5: Rule out firewall/SELinux, then reload

sudo iptables -L -n | grep -E '8000|REJECT|DROP'
sudo ausearch -m avc -ts recent | grep nginx   # RHEL family
sudo nginx -t && sudo systemctl reload nginx

Fix any REJECT rule or SELinux boolean, then validate and reload.

Example Root Cause Analysis

api.example.com returns 502 on every request after a Python upgrade. The backend service shows active (running), yet NGINX still refuses.

The error log:

[error] 6120#6120: *40711 connect() failed (111: Connection refused) while connecting to upstream, upstream: "http://[::1]:8000/v1/users"

The upstream: is [::1]:8000 — IPv6. Checking what the app bound:

sudo ss -ltnp '( sport = :8000 )'
grep -RnE 'proxy_pass' /etc/nginx/conf.d/app.conf
LISTEN 0 2048 127.0.0.1:8000 ... uvicorn
proxy_pass http://localhost:8000;

proxy_pass http://localhost:8000; resolved localhost to IPv6 ::1, but uvicorn only listens on IPv4 127.0.0.1. The connect to [::1]:8000 is refused because nothing listens there.

Fix: use the explicit IPv4 address, validate, and reload:

# change proxy_pass http://localhost:8000; -> http://127.0.0.1:8000; then:
sudo nginx -t
sudo systemctl reload nginx

The upstream now resolves to the listening address and requests return 200.

Prevention Best Practices

  • Use explicit 127.0.0.1 (or [::1]) in proxy_pass rather than localhost, so NGINX never dials the wrong IP family when the backend binds only one.
  • Template the backend’s listen port/socket and NGINX’s proxy_pass from a single source so a redeploy cannot move the port out from under NGINX.
  • Add a deploy-time health check (curl -f http://127.0.0.1:<port>/healthz) that fails the release if the backend is not listening where NGINX expects.
  • For PHP, key the fastcgi_pass socket to the exact PHP version you run and update it as part of any PHP upgrade.
  • Distinguish errnos when alerting: 111 = restart/redeploy the backend; 110 = firewall; 13 = SELinux. They have different fixes.
  • For a fast read on a refused-upstream incident, the free incident assistant can match the errno and upstream target to the likely cause. More fixes live in the NGINX guides.

Quick Command Reference

# Read the upstream and errno
sudo grep -E "connect\(\) failed" /var/log/nginx/error.log | tail -5

# Probe the upstream directly
curl -sv http://127.0.0.1:8000/ 2>&1 | tail -5
sudo ss -ltnp '( sport = :8000 )'

# Backend service state
sudo systemctl status app-gunicorn php8.2-fpm
sudo journalctl -u app-gunicorn --no-pager | tail -30

# Compare config vs reality
grep -RnE 'proxy_pass|fastcgi_pass' /etc/nginx/conf.d/ /etc/nginx/sites-enabled/
ls -l /run/php/

# Rule out firewall / SELinux
sudo iptables -L -n | grep -E '8000|REJECT|DROP'
sudo ausearch -m avc -ts recent | grep nginx

# Validate and reload
sudo nginx -t && sudo systemctl reload nginx

Conclusion

connect() failed (111: Connection refused) means NGINX dialed an address where nothing is listening. The usual root causes:

  1. The backend process is down or crashed.
  2. proxy_pass/fastcgi_pass targets the wrong port.
  3. An IP-family mismatch (localhost -> ::1 vs a backend on 127.0.0.1).
  4. A wrong or missing Unix socket path for PHP-FPM.
  5. A firewall REJECT rule on a remote upstream.
  6. (Looks similar but is errno 13) SELinux denying the connect.

The errno is the key: 111 means a closed port, so restart the backend or fix the address in proxy_pass, then nginx -t and reload. Prefer explicit IPs over localhost to avoid the IPv6 trap.

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.