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.
| Error | Meaning | Direction |
|---|---|---|
| ECONNREFUSED | Arrived, nobody listening | Check service/port |
| ETIMEDOUT | Never arrived or dropped | Check 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:
| Service | Default port |
|---|---|
| MySQL | 3306 |
| PostgreSQL | 5432 |
| Redis | 6379 |
| MongoDB | 27017 |
| Elasticsearch | 9200 |
| RabbitMQ | 5672 |
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.
Related error: ECONNREFUSED vs EADDRINUSE
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
localhostinside a container is not your host: from the container's view, localhost is the container itself. Reaching a host service meanshost.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:3306maps 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
docker ps/systemctl status— is the service alive? (check for crash loops)ss -tlnp | grep <port>— what address and port are actually being listened on?- Bound to
127.0.0.1or0.0.0.0/::? Remote and IPv6 clients need a matching bind - Container → host service: use
host.docker.internal, not localhost - Cloud: check security group rules; on-box: check iptables REJECT
- Distinguish REFUSED (arrived, nobody home) from TIMEOUT (never arrived) — opposite directions
- 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.
Related Tools
Related Articles
Linux Crontab in Practice: Commands, Logs, and Debugging
Crontab beyond syntax: three must-know commands, /etc/cron.d vs user crontabs, five-step debugging, PATH/timezone/% escaping, MAILTO, and what to do when you need second-level scheduling.
docker run to docker-compose: The Complete Migration Guide (with Flag Map)
Migrate docker run commands to docker-compose.yml: five advantages of compose over raw run, a flag-by-flag mapping table (ports, volumes, env, restart, networks), the classic pitfalls (bare -e passthrough, anonymous volumes, --rm semantics), and an online converter that runs locally in your browser.
Permission denied (publickey): 6 Reasons Git Push Fails Over SSH (and the Fix for Each)
git clone or push rejected with Permission denied (publickey) fatal: Could not read from remote repository? Covers missing keygen, key not loaded in the agent, public key not added to GitHub/GitLab, multi-account key routing with ~/.ssh/config, deploy key limits, and wrong remote URLs — with ssh -v diagnostics.