Skip to content
DevOps2026-09-086 min read

ECONNREFUSED: Connection Refused — 5 Causes Explained (Including Docker)

Symptom: every language, same error

# Node.js
Error: connect ECONNREFUSED 127.0.0.1:3306
    at TCPConnectWrap.afterConnect (node:net:1607:16)

# curl
curl: (7) Failed to connect to 127.0.0.1 port 6379: Connection refused

# Java
java.net.ConnectException: Connection refused (Connection refused)

One-line diagnosis: the network reached the machine, but no process is listening on the port you dialed. The OS replies with a TCP RST. Note the contrast with "timeout": ETIMEDOUT means packets never arrive or get dropped; ECONNREFUSED means they arrived but nobody answered. That distinction alone decides where to investigate.

ErrorMeaningDirection
ECONNREFUSEDArrived, nobody listeningCheck service/port
ETIMEDOUTNever arrived or droppedCheck network/firewall

Five causes, ranked by frequency

1. The service simply isn't running (most common)

systemctl status mysql        # Linux services
docker ps                     # containers up? restarting?
brew services list            # macOS

The high-frequency trap is a container in a crash-restart loop: check whether STATUS in docker ps -a says Restarting (1) 5 seconds ago or Up. The former means the container keeps dying — of course the port is dead.

2. Wrong port from memory

Default port cheat sheet:

ServiceDefault port
MySQL3306
PostgreSQL5432
Redis6379
MongoDB27017
Elasticsearch9200
RabbitMQ5672

Two classic mistakes: Redis started with a custom --port override, or MySQL with a modified port= in my.cnf. Trust the actual listener, not your memory:

ss -tlnp | grep -E '3306|6379'    # Linux
lsof -nP -iTCP:3306 -sTCP:LISTEN  # macOS

3. The service binds 127.0.0.1 only, and you connect from outside

ss -tlnp | grep 5432
# LISTEN 127.0.0.1:5432   ← local connections only
# LISTEN 0.0.0.0:5432     ← any source
# LISTEN [::]:5432        ← any source, IPv6

If it's 127.0.0.1:port, every remote connection is refused. Fix per service: comment out bind-address=127.0.0.1 in MySQL, set listen_addresses = '*' in PostgreSQL, change bind 0.0.0.0 in Redis (and always pair that with requirepass — an exposed Redis is ransomware bait).

4. Docker: localhost inside a container is not your host

When an app inside a container connects to a service on the host, 127.0.0.1 refers to the container itself:

# Two correct patterns for an app container reaching a database

# Pattern 1: both on a docker network — use the service name (recommended)
spring.datasource.url: jdbc:mysql://interview-mysql:3306/mydb

# Pattern 2: container reaching a service on the host
# macOS/Windows Docker Desktop has a dedicated hostname:
jdbc:mysql://host.docker.internal:3306/mydb
# Linux hosts (Docker 20.10+) need extra_hosts:
services:
  app:
    extra_hosts:
      - "host.docker.internal:host-gateway"

The reverse direction — host connecting into a container — only works through an explicit port mapping like docker run -p 127.0.0.1:3306:3306. Without -p, curl localhost:3306 from the host is guaranteed ECONNREFUSED.

5. Firewall actively REJECTing

An iptables REJECT rule sends back an RST, which looks exactly like "nobody listening" (DROP would show as a timeout instead):

sudo iptables -L -n | grep -i reject

Cloud servers add another layer: security groups. If the Alibaba/Tencent/AWS console rule doesn't allow the port, you get refused or timed-out connections while everything looks fine inside the server via ss. This is the classic "works locally, dead in the cloud" cause.

The diagnostic route (three layers, bottom-up)

# Layer 1: is anyone listening on the port?
ss -tlnp | grep <port>

# Layer 2: does it connect from this machine?
nc -zv 127.0.0.1 <port>

# Layer 3: does it connect from inside the target network/container?
docker exec -it <container> sh -c 'nc -zv <target-host> <port>'

Nobody at layer 1 → causes 1/2. Local works, remote doesn't → causes 3/5. Fails only inside Docker → cause 4. Three layers and you've localized it.

Two ends of the same port: refused while connecting out is ECONNREFUSED (nothing listening on the far side); refused while binding to listen is EADDRINUSE (the port is already taken on this side). For the latter, see Port already in use: EADDRINUSE troubleshooting.

The container-era variants nobody warns you about

  • localhost inside a container is not your host: from the container's view, localhost is the container itself. Reaching a host service means host.docker.internal (Mac/Windows) or the docker bridge IP (Linux) — the single most common containerized ECONNREFUSED
  • 127.0.0.1 vs 0.0.0.0 on the server side: a service bound to 127.0.0.1 refuses connections from other containers/networks even when the port is "open" — bind address decides reachability, not the firewall
  • Port mapping direction: -p 3306:3306 maps host→container; connecting to container:3306 from the host works, but container-to-container must use the compose network name (db:3306), not the host port — mixing the two mental models produces "works from my machine, refused in CI"

IPv6 localhost surprises (::1 vs 127.0.0.1)

On dual-stack hosts, localhost may resolve to ::1 first. If your server only called listen(3306, '127.0.0.1'), clients that dial localhost over IPv6 get ECONNREFUSED even though ss shows an IPv4 listener and mysql -h 127.0.0.1 works. Fixes: bind :: / 0.0.0.0 deliberately, or force the client URL to 127.0.0.1. Always print what DNS returned (getent hosts localhost / dscacheutil) before blaming the service.

Kubernetes and service DNS

Inside a cluster, ECONNREFUSED to my-svc.my-ns.svc.cluster.local:8080 usually means: the Service selector matches zero Ready pods, you targeted the wrong port name, or you dialed the pod IP after it was recycled. Prefer Service DNS + named ports; check kubectl get endpoints — empty Endpoints with a live Deployment is the cluster twin of “container crash-looping.” NetworkPolicies that REJECT (not DROP) also present as refused.

App-level checks that skip the TCP layer

A process can listen and still be useless: TLS handshake required on a plain HTTP client, HTTP/2-only server, or a reverse proxy that accepts TCP then 502s. After nc -zv succeeds, hit the real protocol with curl or the browser API Tester. Compose healthcheck failing while the port is open is another frequent trap — orchestrators mark the container unhealthy and stop routing even though raw TCP connects.

When documenting networks for teammates, sketch CIDR / published ports with the CIDR Calculator and keep ports: / expose: consistent in Run to Compose output so “works in docker run, refused in compose” stop being archaeology.

Checklist

  1. docker ps / systemctl status — is the service alive? (check for crash loops)
  2. ss -tlnp | grep <port> — what address and port are actually being listened on?
  3. Bound to 127.0.0.1 or 0.0.0.0 / ::? Remote and IPv6 clients need a matching bind
  4. Container → host service: use host.docker.internal, not localhost
  5. Cloud: check security group rules; on-box: check iptables REJECT
  6. Distinguish REFUSED (arrived, nobody home) from TIMEOUT (never arrived) — opposite directions
  7. After TCP works: verify TLS/HTTP with a real client; in k8s, verify Endpoints are non-empty

Triage notes

The article opens with the REFUSED vs TIMEOUT comparison table before the five causes because that distinction decides the entire direction: if packets arrive and nobody listens, investigate the service; if they can't arrive, investigate the network — get this backwards and both layers of triage are wasted. The three-layer diagnostic path is paired with the cause numbering: layer one's "nobody listening" maps to causes 1/2, works-locally-fails-remotely to causes 3/5, and fails-inside-container to cause 4. The firewall entry points out that REJECT answers with an RST, indistinguishable from "nothing listening" — only DROP shows up as a timeout — which is the common source of REFUSED being misread as a service problem.


Provided by ToolVault. Related tools: Dockerfile Generator, Run to Compose, CIDR Calculator, API Tester. Related reading: Port already in use (EADDRINUSE). See the homepage for more developer tools.