Node.js ECONNREFUSED: connection refused
Connection refused
Verified against Node.js docs: net.createConnection, Node.js docs: errors, POSIX connect(2) man page · Updated May 2026
> quick_fix
Nothing is listening on the host:port you connected to, or a firewall sent a TCP RST. Check the target service is actually up: nc -zv host port. Then verify the address (localhost vs 127.0.0.1 vs container hostname) matches where the service binds.
# Test reachability
nc -zv localhost 5432
# Connection to localhost port 5432 [tcp/postgres] succeeded!
# Or use curl for HTTP
curl -v http://localhost:3000/healthWhat causes this error
When Node calls connect(2), the kernel sends a TCP SYN to the target. If no socket is listening on (host, port), the target replies with a RST packet. The kernel translates this into ECONNREFUSED (errno 61 macOS, 111 Linux). Common causes: target service crashed, wrong port, listening on 127.0.0.1 instead of 0.0.0.0, or a firewall rule actively rejecting (vs silently dropping).
How to fix it
- 01
step 1
Verify the target service is running
Don't assume. ps aux | grep <service> on the target machine, or for Docker, docker ps | grep <container>. A 'connection refused' on what should be a healthy service is your first signal that the service is actually dead.
# On the target host ss -tlnp | grep :5432 lsof -iTCP -sTCP:LISTEN | grep postgres - 02
step 2
Check what address the target binds to
A service bound to 127.0.0.1 only accepts connections from the same host. If your client is on a different host or in a different container, you need bind to 0.0.0.0 (all interfaces) or the specific reachable IP.
ss -tlnp | grep :5432 # LISTEN 0 128 127.0.0.1:5432 <- localhost only # LISTEN 0 128 0.0.0.0:5432 <- all interfaces - 03
step 3
Test reachability from the client side
Use nc -zv or curl from the client host to confirm. If nc says 'connection refused' too, the issue is between you and the target, not in your Node code.
nc -zv 10.0.0.5 5432 # nc: connect to 10.0.0.5 port 5432 (tcp) failed: Connection refused - 04
step 4
Check container networking
Docker Compose services reach each other by service name, not localhost. From service A, postgresql://db:5432 not localhost:5432. Each container has its own loopback. Same applies to Kubernetes pods: use the Service DNS name.
# docker-compose.yml services: api: environment: DATABASE_URL: postgresql://user:pass@db:5432/app # not localhost db: image: postgres:17 - 05
step 5
Add retry with exponential backoff for boot-time races
On startup, your API container often starts before Postgres finishes initialising. ECONNREFUSED at t+0 is normal. Retry with backoff up to 30s, then fail.
async function connectWithRetry(fn, maxAttempts = 10) { for (let i = 0; i < maxAttempts; i++) { try { return await fn() } catch (e) { if (e.code !== 'ECONNREFUSED') throw e await new Promise(r => setTimeout(r, Math.min(1000 * 2 ** i, 30000))) } } throw new Error('Connection refused after retries') } - 06
step 6
Distinguish from ETIMEDOUT and EHOSTUNREACH
ECONNREFUSED = host reachable, port closed (active reject). ETIMEDOUT = host not responding (firewall drop or down). EHOSTUNREACH = no route to host. The fix differs: refused = start the service; timeout = check firewalls; unreach = check routing.
client.on('error', (err) => { switch (err.code) { case 'ECONNREFUSED': console.log('Service is down or wrong port') case 'ETIMEDOUT': console.log('Network or firewall issue') case 'EHOSTUNREACH': console.log('Routing problem') } })
Why ECONNREFUSED happens at the runtime level
When connect(2) sends a TCP SYN to a port with no listening socket, the target host's kernel responds with a RST packet (active reject) per RFC 793. The originating kernel propagates this to the application as errno ECONNREFUSED. The same error fires when a host-based firewall (iptables -j REJECT, ufw default reject) generates a synthetic RST or ICMP port-unreachable. Note that iptables -j DROP causes ETIMEDOUT instead, since no rejection packet is returned. Service crashes, wrong bind address, and stopped containers all manifest as ECONNREFUSED at the kernel level.
Common debug mistakes for ECONNREFUSED
- Connecting to localhost from inside a Docker container expecting to reach a host-machine service.
- Service is running but bound to 127.0.0.1; client on another host or container cannot reach it.
- API container starts before Postgres health check passes; first connection attempts fire and fail.
- Firewall rule was REJECT instead of ACCEPT on a recently locked-down port; the service is up but unreachable.
- Wrong port in connection string (5432 vs 5433) after a docker-compose port remap; service is alive on a different port.
When ECONNREFUSED signals a deeper problem
Persistent ECONNREFUSED in production points at missing health-check and dependency-ordering primitives. The architectural fix is readiness probes (Kubernetes) or healthcheck+depends_on conditions (Docker Compose) so services only receive traffic once their dependencies are confirmed responsive. Adding circuit breakers (opossum, undici with retry policy) prevents one downstream outage from cascading into thread-pool exhaustion upstream. If the same dependency fails repeatedly with ECONNREFUSED, the real issue is missing fault-tolerant infrastructure: read replicas, connection pooling at the proxy layer, or a service mesh that handles retries with deadline budgets.
Frequently asked questions
Why does ECONNREFUSED only happen sometimes?
Intermittent ECONNREFUSED usually means a race condition during service startup. The client tries to connect before the server has finished binding its listener. Common during docker-compose up, where dependent services boot in parallel by default. Use depends_on with condition: service_healthy and a HEALTHCHECK in the dependency. In Kubernetes, use readiness probes. In code, retry with exponential backoff for the first 30 seconds, then escalate.
What is the difference between ECONNREFUSED and ECONNRESET?
ECONNREFUSED happens during the initial three-way handshake: the SYN is RST-ed before any connection is established. ECONNRESET happens later, on an already-established connection: the peer sent RST mid-stream, usually because it crashed or its OS aborted a stuck socket. ECONNREFUSED means 'never connected'; ECONNRESET means 'connected then yanked'. The debugging path is different: refused points at service availability, reset points at mid-flight failures or load balancers timing out idle connections.
Why does my Node app see ECONNREFUSED to localhost:3000 in Docker?
Inside a container, localhost (127.0.0.1) refers to the container's own loopback interface, not the host. A service running on the host at port 3000 is invisible. Use host.docker.internal on macOS/Windows Docker Desktop, or pass --network host on Linux to share the host's network namespace. For container-to-container traffic, use the Compose service name as the hostname; Compose runs an embedded DNS server that resolves service names to container IPs.
Should I retry forever on ECONNREFUSED?
No. Retry with bounded attempts and exponential backoff, then fail loudly. Infinite retries hide a real outage and create cascading silent failures. A reasonable default: retry 5-10 times with backoff from 100ms to 30s, then surface the error and let the orchestrator restart the process. For boot-time dependency races specifically (your DB starting alongside your API), retry up to 60 seconds is reasonable; beyond that, fix the deployment ordering.