What you'll learn
- Quick answer
- What ECONNREFUSED Actually Means on the Wire
- Cause 1: Nothing Is Listening on That Port Yet
- Cause 2: You're Talking to the Wrong Host or Interface
- Cause 3: The Process Crashed, or Something Is Actively Blocking It
- How to Debug It in Under a Minute
- The fetch() Quirk, and How This Differs From ETIMEDOUT / ECONNRESET
- FAQ
Quick Answer
ECONNREFUSED means a TCP connection attempt reached a live machine, but no process was listening on the port that was tried, so the operating system actively rejected the connection. It's almost always one of three things: the server isn't running yet or has crashed, the request is going to the wrong host or port, or something between the client and the target — a firewall, a Docker network boundary — is misdirecting it. It's rarely a bug in application logic itself.
What ECONNREFUSED Actually Means on the Wire
Opening a TCP connection is a handshake: the client sends a SYN packet and waits for a SYN-ACK back. If the target machine is reachable but nothing is bound and listening on the requested port, its operating system replies immediately with a TCP RST (reset) packet — and whatever made the request, Node's http module, fetch(), curl, a browser, surfaces that as ECONNREFUSED.
The distinction worth holding onto for everything that follows: this specifically means the packet arrived somewhere with a live TCP/IP stack that actively said no. That's different from a timeout, where no response comes back at all because the packet may never have reached anything real — covered in the last section below.
const http = require('http');
http.get('http://127.0.0.1:59999', (res) => {
console.log(res.statusCode);
}).on('error', (err) => {
console.log(err.message); // connect ECONNREFUSED 127.0.0.1:59999
console.log(err.code); // ECONNREFUSED
});
That's real output from hitting a port nothing was listening on. The message format is always the same shape — connect ECONNREFUSED <host>:<port> — and it's worth reading the host and port directly out of it before guessing what's wrong; it's frequently not what was assumed.
Cause 1: Nothing Is Listening on That Port Yet
The most common real-world cause: the server process hasn't started listening yet. This happens constantly with docker-compose or any setup that starts a frontend and backend together — the backend can take a couple of seconds to boot while the frontend's very first API call fires immediately. It's equally often a server that crashed and is simply no longer running, or a request going to a port the server was never actually bound to, because of a typo or an environment variable that silently fell back to a default.
For the startup-race case specifically, the fix is to retry with backoff instead of assuming a dependency is instantly available the moment the process starts:
async function connectWithRetry(url, attempts = 5, delayMs = 200) {
for (let i = 1; i <= attempts; i++) {
try {
const res = await fetch(url);
return { ok: true, attempt: i, status: res.status };
} catch (err) {
if (err.cause && err.cause.code === 'ECONNREFUSED' && i < attempts) {
await sleep(delayMs);
continue;
}
throw err;
}
}
}
Tested against a server deliberately started 450ms late: the first two attempts logged ECONNREFUSED and retried, and the third succeeded once the server actually came up — { ok: true, attempt: 3, status: 200 }. This exact pattern, retry a fixed number of times with a short delay between attempts, is what health checks and "wait-for-it"-style scripts exist to automate.
Cause 2: You're Talking to the Wrong Host or Interface
A server can accept connections on one address and refuse — or never even receive — them on another, depending entirely on which network interface it's bound to. server.listen(port, '127.0.0.1') only accepts connections arriving via loopback, from the same machine. server.listen(port, '0.0.0.0') accepts connections on any of the machine's interfaces, which matters the instant the client isn't literally the same machine — a Docker container reaching a different container, a phone testing a dev server over the LAN, a load balancer forwarding traffic in from outside.
server.listen(3000, '127.0.0.1'); // only this machine can connect
server.listen(3000, '0.0.0.0'); // any interface, incl. other containers/devices
This was checked directly: a server bound only to 127.0.0.1 answered fine when hit via 127.0.0.1, but a request to the same port on the machine's actual LAN IP address never came back with ECONNREFUSED at all — it simply hung until it timed out. That's the nuance worth keeping straight: a request that never reaches the right network stack tends to hang or time out, not get refused. ECONNREFUSED specifically means the request did reach a live TCP stack with nobody listening there. Inside Docker, the practical fix is nearly always the same: bind the server inside the container to 0.0.0.0, not 127.0.0.1 or localhost, and reference the other container by its service name — never localhost — when connecting from outside it.
Cause 3: The Process Crashed, or Something Is Actively Blocking It
Sometimes the server was answering fine and then stopped — an unhandled exception took the process down, a memory limit killed the container, or a deploy rolled the instance over and the replacement isn't up yet. The first thing worth checking, before digging into anything network-related, is simply whether the process is still alive at all: docker ps, pm2 list, or just checking whether the terminal that was running it is still there and hasn't printed a crash trace.
Separately, a firewall, a cloud security group, or a reverse proxy actively rejecting traffic can also present as ECONNREFUSED rather than a silent timeout, if whatever is doing the rejecting responds with a reset instead of just dropping the packet. Security groups and firewalls are configured either way depending on the provider and the rule, so ECONNREFUSED coming from a cloud VM doesn't automatically mean the application crashed — check the application's own logs and the relevant firewall or security group rule for that port before assuming it's a code problem.
How to Debug It in Under a Minute
A short, ordered checklist gets to the answer fast in almost every case:
- Confirm the server is actually listening on the port that's expected: on Windows,
netstat -ano | findstr :3000; on Mac or Linux,lsof -i :3000. If nothing shows up, that already is the answer — nothing is listening, full stop. - Try the exact same host and port with a tool that isn't the application itself, to rule out an app-side bug in how the request is being made:
curl -v http://localhost:3000/. The-vflag shows the actual TCP-level failure directly, rather than a wrapped application error. - For a container, check whether the port is actually published to the host (the port-mapping column in
docker ps) and whether the calling code uses the other container's service name, notlocalhost, when reaching it from a different container. - Read the error message's host and port literally before assuming anything — a stale or unexpected URL in the error is frequently a misread environment variable, which is a config fix, not a networking one.
The fetch() Quirk, and How This Differs From ETIMEDOUT / ECONNRESET
One thing that trips people up in modern Node code specifically: fetch() doesn't expose err.code directly the way the older http module does. Catching a failed fetch() call gives back a generic TypeError with the message "fetch failed" — the actual underlying network error, including its .code, is nested one level down in err.cause.
try {
await fetch('http://127.0.0.1:59997/api/health');
} catch (err) {
console.log(err.name); // TypeError
console.log(err.message); // fetch failed
console.log(err.cause.code); // ECONNREFUSED
}
That output is real, captured from an actual failed request. Writing if (err.code === 'ECONNREFUSED') against a fetch() error silently never matches — err.code on the outer error is undefined — so any retry or fallback logic gated on it simply never fires. Check err.cause?.code instead.
Worth keeping the three related error codes straight, since the fix for each is different: ECONNREFUSED means the request reached a live host that had nobody listening — usually a startup-timing or configuration problem. ETIMEDOUT means no response arrived at all within the wait window — usually a network, firewall, or wrong-address problem, since the request may never have reached anything real. ECONNRESET means a connection that was working got forcibly closed mid-stream — usually the other side crashed, hit its own timeout, or a proxy in between cut the connection.
