connection refused / ECONNREFUSED 127.0.0.1: a 30-second triage runbook
Errors this covers: Error: connect ECONNREFUSED 127.0.0.1:5432, connect ECONNREFUSED ::1:3000, curl: (7) Failed to connect to localhost port 8080 after 0 ms: Could not connect to server, dial tcp 127.0.0.1:6379: connect: connection refused. They all mean "nothing is listening on that address and port."
| Cause | Check | Fix |
|---|---|---|
| Server process not running | The port is missing from sudo ss -tlnp | Start the service; read its startup error log |
Bound to 127.0.0.1 only (client is remote or outside the container) | ss shows 127.0.0.1:<port> | Bind the app to 0.0.0.0 (or the specific IP) |
localhost resolves to ::1 but the server is IPv4-only | The error address is ::1 | Use 127.0.0.1 in the client, or also bind the server to :: |
Using localhost inside a container to reach another service | The target address and docker ps port mapping | Use the compose service name or host.docker.internal |
| Firewall REJECT rule | sudo nft list ruleset / sudo iptables -L -n | Allow the port |
connection refused is not "no response" — it is an active rejection
Right after a deploy, when curl spits out Connection refused or a Node app dies with ECONNREFUSED 127.0.0.1:5432, the first thing you need to do is understand exactly what this error is.
The key is one sentence. connection refused means the packet reached the target, and the target immediately replied with a TCP RST saying "nobody is listening on that port." In other words, the network is fine. The path is not blocked (that would be a timeout) — you arrived and got turned away at the door. That one fact completely changes the direction of your diagnosis.
There is one exception. If a firewall answers with a REJECT rule instead of silently dropping the packet, it sends back ICMP port-unreachable (the default) or a TCP RST, and the client sees the same refused error (iptables REJECT). If the server is definitely running and you still get refused, check the firewall rules too.
First, distinguish it from neighboring errors
| Error signal | TCP behavior | First-line suspected cause |
|---|---|---|
| connection refused | Immediate RST received (fast) | Process not running / port or bind mismatch |
| timeout (no route, hang) | No response; wait seconds to tens of seconds | Firewall DROP / security group / routing |
| EADDRINUSE | Bind failure at server start | A process already occupying that port |
| 502 Bad Gateway | Proxy is up but upstream refused | Backend is in state 1 or 2 above |
If it's a timeout, start by suspecting the firewall/routing. If it's refused, it is almost always a server-side problem. Refusal is fast; blocking (DROP) is slow. That speed difference alone gets you halfway there.
30-second triage runbook: a 4-step decision tree
Follow this from top to bottom the moment you see the error.
① Is the process running? → ss -tlnp / systemctl status
└ No → start the server (root cause confirmed)
└ Yes ↓
② Is the port/bind correct? → 127.0.0.1:PORT vs 0.0.0.0:PORT in ss output
└ Listening only on 127.0.0.1 but connecting from outside → change the bind (root cause confirmed)
└ Correct ↓
③ Blocked in the middle? → nc -zv / ufw status / docker ps port mapping
└ Firewall/security group/mapping missing → allow the rule (root cause confirmed)
└ Passes ↓
④ Is the name resolving wrong? → check whether localhost resolves to ::1 (IPv6)
└ Listening IPv4 only + IPv6-first resolution → use 127.0.0.1 directlyCopy-paste command table with risk labels
| Label | Command | What it checks/changes |
|---|---|---|
| 🟢 Safe | sudo ss -tlnp | Which process is listening on which IP:port (process names of other users need sudo) |
| 🟢 Safe | systemctl status <svc> | Whether the service is actually active |
| 🟢 Safe | nc -zv host port | Whether a TCP connection to that port succeeds (refused vs timeout) |
| 🟢 Safe | telnet host port | Same purpose when nc is unavailable |
| 🟢 Safe | docker ps | Presence of mapping via the PORTS column (0.0.0.0:8080->80) |
| 🟢 Safe | sudo ufw status | Check ufw inbound allow rules |
| 🟢 Safe | sudo iptables -L -n | Check ACCEPT/DROP/REJECT rules per chain |
| 🟢 Safe | sudo nft list ruleset | Check rules on distros that use nftables |
| 🟡 Caution | sudo ufw allow 8080/tcp | Open an inbound port (changes state) |
| 🟡 Caution | sudo systemctl restart <svc> | Restart the service (causes downtime) |
| 🟡 Caution | docker run -p 8080:80 ... | Restart with port mapping reset |
🟢 you can run freely. 🟡 think twice if this is production.
The core trap: 127.0.0.1 bind vs 0.0.0.0 bind
In the field, when something "works locally but is refused in a container or remotely," this is the first thing to check. Let's reproduce it.
# A. Bind to loopback only
python -m http.server --bind 127.0.0.1 8000
# B. Bind to all interfaces
python -m http.server --bind 0.0.0.0 8000Start each one and run ss -tlnp — the difference is obvious.
# Case A
LISTEN 0 5 127.0.0.1:8000 0.0.0.0:* users:(("python",pid=...))
# Case B
LISTEN 0 5 0.0.0.0:8000 0.0.0.0:* users:(("python",pid=...))A is 127.0.0.1:8000, meaning it answers only from inside the same machine. If you connect from another host or from outside a container, the kernel says "no listener on that port for this IP" → immediate RST → connection refused. B, on the other hand, also accepts packets that arrive on an external IP.
One line from the field: Framework defaults are the trap. Flask
app.run()listens only on127.0.0.1unless you pass a host (Flask docs), and many other dev servers also default to loopback. If you get "fine on my laptop, refused only on EC2/Docker," don't start with the code — check the bind address withss -tlnpfirst. I've burned days of time on this.
Same error, different faces: message mapping by language/tool
Everything below is the same TCP RST signal. Only the message differs; the diagnostic path is identical.
| Tool | Error message |
|---|---|
| curl 8.x | curl: (7) Failed to connect to localhost port 8080 after 0 ms: Could not connect to server (with -v: connect to 127.0.0.1 port 8080 ... failed: Connection refused) |
| older curl | curl: (7) Failed to connect to localhost port 8080: Connection refused |
| Node | Error: connect ECONNREFUSED 127.0.0.1:5432 |
Node (connecting to localhost) | AggregateError [ECONNREFUSED] containing connect ECONNREFUSED ::1:5432 and connect ECONNREFUSED 127.0.0.1:5432 |
| Go | dial tcp 127.0.0.1:6379: connect: connection refused |
| psql | psql: error: connection to server at "127.0.0.1", port 5432 failed: Connection refused (older: could not connect to server: Connection refused) |
| redis-cli | Could not connect to Redis ... Connection refused |
Diagnosis by case
- curl:
curl -v http://host:port→ if refused immediately, the server is down or the port is mistyped. Cross-check withnc -zv host port. - Node ECONNREFUSED 127.0.0.1:5432: Common when the DB host is
localhostbut the DB is in a container or remote. Check the host in the connection string, thenss -tlnp | grep 5432. - Node shows
::1: Since Node 17,localhostaddresses are used in the order the OS returns them (verbatim), so::1can come first (dns docs). Since Node 20,autoSelectFamilydefaults to true, so Node tries the IPv6 and IPv4 addresses in turn and, if all fail, reports them together as anAggregateError(net docs). As of October 2026 the LTS lines are Node 24 (Active) and Node 22 (Maintenance); Node 20 reached end of life on 2026-04-30 (Node.js release schedule). - Go dial tcp: Same. Also common: the app starts before a dependent service is up (fix with
depends_on/ health checks). - psql/redis-cli: The server is up but listening only on
127.0.0.1, while the client connects from outside → check the bind orbindsettings (redisbind 127.0.0.1, postgreslisten_addresses).
Docker and cloud-specific traps
There's a clear reason refused spikes in container environments.
-
The app inside the container binds to
127.0.0.1→ evendocker run -p 8080:80gets refused.-pforwards traffic from the host to the container's external interface, but if the app is only listening on the container's loopback, there is no listener to reach. Always bind to0.0.0.0inside a container. -
Container → host connections:
localhostinside a container is the container itself, not the host. To reach a host service, usehost.docker.internal. Docker Desktop (Mac/Windows) resolves it automatically; on Linux Docker Engine, add--add-host=host.docker.internal:host-gateway(extra_hostsin compose) (Docker docs). For another container in the same compose project, connect by service name (for exampledb:5432). -
AWS security groups / inbound: If a security group is blocking you, you usually get a timeout (DROP), not refused. If you see refused and you're blaming the security group, you're looking in the wrong direction. Exception: an NLB/target group sending health checks to a closed port can surface as refused.
-
IPv6-first resolution: If
localhostresolves to::1first but the server is listening only on IPv4 (0.0.0.0), you get refused. Temporary workaround: explicitly use127.0.0.1. Binding the server to::(dual-stack) as well fixes it properly.
Conclusion: 4-step checklist card
[ ] ① Confirm process/port listening with ss -tlnp (if none → start it)
[ ] ② Check bind address 127.0.0.1 vs 0.0.0.0 (loopback only → 0.0.0.0)
[ ] ③ nc -zv / ufw status / docker ps mapping (if blocked → allow the rule/mapping)
[ ] ④ Whether localhost resolves to ::1 (IPv6 issue → specify 127.0.0.1)refused is almost always a server-side problem — specifically "it isn't running, or it's running on the wrong address." Top to bottom, 30 seconds and you're done.
FAQ
Q. How do I quickly tell connection refused from a timeout?
A. Run nc -zv host port. If it fails immediately with refused, the server is down or the port is wrong (server-side). If it hangs for several seconds then fails, it's a firewall DROP, security group, or routing problem. Speed is the clue.
Q. It works locally but I only get refused in a container/remotely.
A. Usually the app is bound to 127.0.0.1. Confirm with ss -tlnp and change it to 0.0.0.0. In a container, the inner app must bind to 0.0.0.0 for -p mapping to work.
Q. ECONNREFUSED 127.0.0.1:5432 — the DB address is correct, so why refused?
A. (1) The DB process isn't running, (2) the DB is in a container/remote but the host is set to localhost, or (3) postgres listen_addresses / redis bind is restricted to loopback. Start by checking the actual listen address with ss -tlnp | grep 5432.
Sources and checks vary by article. Only articles with a recorded check date, scope and sources below were checked, and only within that scope. For other articles, verify against the current docs for your version before applying anything, and email us about errors. Editorial policy · email us
This article has no recorded human check (date and scope) yet.
Comments
Be the first to comment.