ERR_CONNECTION_REFUSED / ECONNREFUSED (tested)
ERR_CONNECTION_REFUSED (Chrome) and ECONNREFUSED (Node, Python) mean the same thing: nothing was listening on that address and port, so the OS said no. Most of the time the server is down or on another port. The sneaky case is when it is running, but on 127.0.0.1 while the client tries ::1, or the other way round. We measured every combination.
1. What "connection refused" means
The client sent a TCP connect (SYN) and the other side answered with a reset right away. On the same machine that happens when no socket is listening on that exact IP address and port. The request never reached your app, so there is no HTTP status, no headers and no server log line.
It fails fast. Every refused connect in our tests failed in 0 ms. If you wait seconds and then fail, that is a timeout (firewall dropping packets), not a refusal. If the connection opens and then dies, see ERR_CONNECTION_RESET / ECONNRESET.
2. The exact error in each client
We pointed each client at a port with nothing listening.
| Client | What it reported |
|---|---|
| Chromium 153 navigation | net::ERR_CONNECTION_REFUSED (page shows "This site can't be reached") |
Chromium 153 fetch() | JS gets only TypeError: Failed to fetch. The console shows Failed to load resource: net::ERR_CONNECTION_REFUSED |
| curl 8.14.1 | exit code 7, curl: (7) Failed to connect to 127.0.0.1 port 8991 after 0 ms: Could not connect to server |
Node 24 fetch | TypeError: fetch failed; err.cause.code = ECONNREFUSED, err.cause.message = connect ECONNREFUSED 127.0.0.1:8991 |
Node 24 fetch to localhost | err.cause is an AggregateError with code ECONNREFUSED and an empty message. The details are in err.cause.errors: connect ECONNREFUSED ::1:8991 and connect ECONNREFUSED 127.0.0.1:8991 |
| Python urllib | URLError: <urlopen error [Errno 111] Connection refused> |
| Python requests 2.32.3 | ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8991): Max retries exceeded ... Failed to establish a new connection: [Errno 111] Connection refused |
Note the Node localhost case. If you only log err.cause.message, you log an empty string. Log err.cause.code and err.cause.errors too.
try {
await fetch('http://localhost:3000/');
} catch (err) {
const c = err.cause;
console.log(c?.code, c?.message || c?.errors?.map(e => e.message));
}
3. localhost is two addresses
On our box /etc/hosts maps localhost to both 127.0.0.1 and ::1. The system resolver returns ::1 first. So did Python's socket.getaddrinfo. Node 24's dns.getDefaultResultOrder() is verbatim (the default since Node 17), so dns.lookup('localhost') also returned ::1 first.
That matters on both sides. A client asking for localhost tries ::1 first. A server told to listen on localhost picks one address only: our Node server given host: 'localhost' bound to [::1]:8990 and nothing else.
4. The matrix: bind address vs client target
We ran a Node http server bound to each address on port 8990, then connected with curl, Node fetch, Python urllib, Python requests, and Chromium (navigation and fetch). All five clients agreed in every cell.
| Server binds to | ss -ltn shows | localhost | 127.0.0.1 | [::1] |
|---|---|---|---|---|
127.0.0.1 | 127.0.0.1:8990 | OK (via 127.0.0.1) | OK | Refused |
::1 | [::1]:8990 | OK (via ::1) | Refused | OK |
localhost (Node) | [::1]:8990 | OK (via ::1) | Refused | OK |
0.0.0.0 | 0.0.0.0:8990 | OK (via 127.0.0.1) | OK | Refused |
:: (dual-stack) | *:8990 | OK (via ::1) | OK (server sees ::ffff:127.0.0.1) | OK |
:: with ipv6Only: true | [::]:8990 | OK (via ::1) | Refused | OK |
Three things stand out:
0.0.0.0is IPv4 only. It means "all IPv4 addresses". It does not accept[::1].::is the only bind that accepted all three targets, because Linux lets an IPv6 socket also take IPv4 (our box hasnet.ipv6.bindv6only = 0). Turn onipv6Onlyand 127.0.0.1 is refused again.localhostas a client target worked in every row. Modern clients try the next address when the first is refused. curl's verbose log shows it:
$ curl -v http://localhost:8990/ # server on 127.0.0.1 only
* Trying [::1]:8990...
* connect to ::1 port 8990 from ::1 port 59782 failed: Connection refused
* Trying 127.0.0.1:8990...
* Connected to localhost (127.0.0.1) port 8990
5. The Node 17+ localhost trap
Node 17 switched DNS results to verbatim order, so localhost became ::1 first. Servers that only listened on 127.0.0.1 then got ECONNREFUSED ::1:PORT. In Node 24 the fallback hides this for most code: net.getDefaultAutoSelectFamily() is true (250 ms per attempt), and fetch fell back to 127.0.0.1 in our test.
The trap still exists when the fallback is off. With a server on 127.0.0.1 only:
net.connect({ port: 8990, host: 'localhost', autoSelectFamily: false })
// error: ECONNREFUSED connect ECONNREFUSED ::1:8990
net.connect({ port: 8990, host: 'localhost', autoSelectFamily: true })
// connected to 127.0.0.1
Libraries that pass their own lookup or family, older Node versions and other runtimes can still hit it. If your error says ::1 and your server listens on 127.0.0.1, this is it.
6. Defaults of common dev servers
- Flask 3.1.3
app.run()bound to127.0.0.1:8990only.curl http://[::1]:8990/was refused;localhostworked through the fallback. - Python 3.13
python3 -m http.serverwith no-bbound to0.0.0.0on our box (it printed "Serving HTTP on 0.0.0.0"). So[::1]is refused. - Node
listen(port, 'localhost')bound to::1only, as in the matrix.
7. Docker port publishing (from the Docker docs, not measured)
-p 8080:80publishes on all host interfaces.-p 127.0.0.1:8080:80publishes on IPv4 loopback only, so[::1]:8080on the host may be refused.- The app inside the container must listen on
0.0.0.0(or::), not127.0.0.1. Container loopback is not the host's loopback, so a server bound to 127.0.0.1 inside cannot be reached through the published port. - Inside a container,
localhostis the container itself. To reach another container use its service name; to reach the host, Docker Desktop provideshost.docker.internal.
8. How to fix it
- Is it running?
ss -ltn | grep :PORT(Linux) orlsof -iTCP:PORT -sTCP:LISTEN(macOS). No line means nothing listens. Check the server's own log for a crash or "address in use". - Right port? Compare the port in the error with the one the server printed.
- Right address? Compare the address in the error (
::1or127.0.0.1) with thessoutput. Use the matrix above. - Pick one fix: make the client use the exact address the server binds (
http://127.0.0.1:3000), or bind the server to::so both families work. Bind to0.0.0.0/::only if you mean to expose it on the network. - Proxy or container in between? If nginx shows 502 Bad Gateway, its error log (per the nginx docs and common reports, not measured) names the upstream with
(111: Connection refused). Apply steps 1 to 3 to the upstream address.
Once it connects, check the real response with our HTTP headers tool.
Frequently asked questions
What is the difference between ERR_CONNECTION_REFUSED and ECONNREFUSED?
None in cause. Chrome reports a refused TCP connect as net::ERR_CONNECTION_REFUSED; Node reports it as code ECONNREFUSED; Python as Errno 111 Connection refused; curl as exit code 7. All mean no socket was listening on that address and port.
Why does localhost work but 127.0.0.1 is refused?
Your server is listening on ::1 only. In our test, a Node server given host 'localhost' bound to [::1] alone. localhost resolves to ::1 first, so it connects, while 127.0.0.1 is refused. Bind to :: or 127.0.0.1 explicitly.
Why does Node say ECONNREFUSED ::1 when my server is running?
The server listens on 127.0.0.1 and Node resolved localhost to ::1 first (verbatim order since Node 17). Node 24 normally falls back to 127.0.0.1, but with autoSelectFamily false we got connect ECONNREFUSED ::1:8990. Use 127.0.0.1 in the URL or bind the server to ::.
Does binding to 0.0.0.0 accept IPv6 too?
No. In our test a server on 0.0.0.0 accepted localhost and 127.0.0.1 but refused [::1]. Binding to :: on Linux accepted all three, because IPv6 sockets also take IPv4 unless ipv6Only is set.
Why does browser fetch only say Failed to fetch?
Browsers hide network error details from JavaScript. In Chromium 153 the script got TypeError: Failed to fetch, while the DevTools console showed net::ERR_CONNECTION_REFUSED. Check the console or Network tab, or try the URL with curl.
Written with AI assistance. Measured on 2026-10-02 on Debian Linux with headless Chromium 153.0.8010.12 (Playwright), Node.js 24.21.0, curl 8.14.1, Python 3.13.5 (urllib, requests 2.32.3) and Flask 3.1.3. Test servers were Node http servers bound to six different addresses. Docker parts are from the Docker docs, not measured. PlainHeaders is our extension; curl, Python, Node.js and Chromium are free.