Home/Infrastructure/connection refused / ECONNREFUSED 127.0.0.1: a 30-second diagnostic runbook
Infrastructureconnection refusedECONNREFUSED

connection refused / ECONNREFUSED 127.0.0.1: a 30-second diagnostic runbook

A 4-step runbook that triages connection refused and ECONNREFUSED 127.0.0.1 in 30 seconds. Diagnose a downed server, port mismatch, firewall, or bind address (127.0.0.1 vs 0.0.0.0) with copy-paste commands, including curl, Node, Go, and psq

connection refused / ECONNREFUSED 127.0.0.1: a 30-second diagnostic runbook
Contents
  1. connection refused is not "no response" — it is an active rejection
  2. 30-second triage runbook: a 4-step decision tree
  3. The core trap: 127.0.0.1 bind vs 0.0.0.0 bind
  4. Same error, different faces: message mapping by language/tool
  5. Docker and cloud-specific traps
  6. Conclusion: 4-step checklist card
  7. FAQ

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."

CauseCheckFix
Server process not runningThe port is missing from sudo ss -tlnpStart 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-onlyThe error address is ::1Use 127.0.0.1 in the client, or also bind the server to ::
Using localhost inside a container to reach another serviceThe target address and docker ps port mappingUse the compose service name or host.docker.internal
Firewall REJECT rulesudo nft list ruleset / sudo iptables -L -nAllow 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 signalTCP behaviorFirst-line suspected cause
connection refusedImmediate RST received (fast)Process not running / port or bind mismatch
timeout (no route, hang)No response; wait seconds to tens of secondsFirewall DROP / security group / routing
EADDRINUSEBind failure at server startA process already occupying that port
502 Bad GatewayProxy is up but upstream refusedBackend 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.

CODE
① 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 directly

Copy-paste command table with risk labels

LabelCommandWhat it checks/changes
🟢 Safesudo ss -tlnpWhich process is listening on which IP:port (process names of other users need sudo)
🟢 Safesystemctl status <svc>Whether the service is actually active
🟢 Safenc -zv host portWhether a TCP connection to that port succeeds (refused vs timeout)
🟢 Safetelnet host portSame purpose when nc is unavailable
🟢 Safedocker psPresence of mapping via the PORTS column (0.0.0.0:8080->80)
🟢 Safesudo ufw statusCheck ufw inbound allow rules
🟢 Safesudo iptables -L -nCheck ACCEPT/DROP/REJECT rules per chain
🟢 Safesudo nft list rulesetCheck rules on distros that use nftables
🟡 Cautionsudo ufw allow 8080/tcpOpen an inbound port (changes state)
🟡 Cautionsudo systemctl restart <svc>Restart the service (causes downtime)
🟡 Cautiondocker 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.

Bash
# 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 8000

Start each one and run ss -tlnp — the difference is obvious.

CODE
# 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 on 127.0.0.1 unless 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 with ss -tlnp first. 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.

ToolError message
curl 8.xcurl: (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 curlcurl: (7) Failed to connect to localhost port 8080: Connection refused
NodeError: 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
Godial tcp 127.0.0.1:6379: connect: connection refused
psqlpsql: error: connection to server at "127.0.0.1", port 5432 failed: Connection refused (older: could not connect to server: Connection refused)
redis-cliCould 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 with nc -zv host port.
  • Node ECONNREFUSED 127.0.0.1:5432: Common when the DB host is localhost but the DB is in a container or remote. Check the host in the connection string, then ss -tlnp | grep 5432.
  • Node shows ::1: Since Node 17, localhost addresses are used in the order the OS returns them (verbatim), so ::1 can come first (dns docs). Since Node 20, autoSelectFamily defaults to true, so Node tries the IPv6 and IPv4 addresses in turn and, if all fail, reports them together as an AggregateError (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 or bind settings (redis bind 127.0.0.1, postgres listen_addresses).

Docker and cloud-specific traps

There's a clear reason refused spikes in container environments.

  1. The app inside the container binds to 127.0.0.1 → even docker run -p 8080:80 gets refused. -p forwards 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 to 0.0.0.0 inside a container.

  2. Container → host connections: localhost inside a container is the container itself, not the host. To reach a host service, use host.docker.internal. Docker Desktop (Mac/Windows) resolves it automatically; on Linux Docker Engine, add --add-host=host.docker.internal:host-gateway (extra_hosts in compose) (Docker docs). For another container in the same compose project, connect by service name (for example db:5432).

  3. 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.

  4. IPv6-first resolution: If localhost resolves to ::1 first but the server is listening only on IPv4 (0.0.0.0), you get refused. Temporary workaround: explicitly use 127.0.0.1. Binding the server to :: (dual-stack) as well fixes it properly.

Conclusion: 4-step checklist card

CODE
[ ] ① 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.

Verification
✦ ✦ ✦
Sources and verification

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.

Published · ·Updated ·

Comments

Be the first to comment.