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/curlagainst the upstream port returns “Connection refused”.- The backend works on a different port/socket than
proxy_passtargets. - 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]) inproxy_passrather thanlocalhost, 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_passfrom 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_passsocket 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:
- The backend process is down or crashed.
proxy_pass/fastcgi_passtargets the wrong port.- An IP-family mismatch (
localhost->::1vs a backend on127.0.0.1). - A wrong or missing Unix socket path for PHP-FPM.
- A firewall
REJECTrule on a remote upstream. - (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.
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.