connection refused / ECONNREFUSED 127.0.0.1: a 30-second triage runbook
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.
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 | ss -tlnp | Which process is listening on which IP:port |
| 🟢 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 | ufw status | Check ufw inbound allow rules |
| 🟢 Safe | iptables -L -n | Check ACCEPT/DROP rules per chain |
| 🟡 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, 80% of "works locally, refused in a container/remotely" is this one thing. 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.
# A의 경우
LISTEN 0 128 127.0.0.1:8000 0.0.0.0:* users:(("python",pid=...))
# B의 경우
LISTEN 0 128 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(), Rails, and some dev servers default to127.0.0.1. 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 | curl: (7) Failed to connect ... Connection refused |
| Node | Error: connect ECONNREFUSED 127.0.0.1:5432 |
| Go | dial tcp 127.0.0.1:6379: connect: connection refused |
| psql | 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. - 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(Mac/Windows; on recent Linux,--add-host). -
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.
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. Nine times out of ten 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.
Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
Comments
Be the first to comment.