Debug guide · you're seeing
ERR_CONNECTION_REFUSEDlocalhost refused to connect.curl: (7) Failed to connect to localhost port 3000 after 0 ms: Couldn't connect to servercurl: (7) Failed to connect to localhost:3000 after 0 ms: Could not connect to serverconnect ECONNREFUSED 127.0.0.1:3000connect ECONNREFUSED ::1:3000
ERR_CONNECTION_REFUSED: Localhost, Docker and Node Fixes
Fix ERR_CONNECTION_REFUSED by checking listeners, localhost IPv4/IPv6, Docker port mappings, firewall rejection, service crashes and Kubernetes endpoints.
On this page
- What it means
- Refused, timeout and reset
- Who sent it?
- Fix it, in order of evidence
- 1. Nothing is listening on the requested port
- 2. The app is bound to loopback inside Docker or a VM
- 3. The port mapping points at the wrong container port
- 4. Localhost chose IPv6, but the server listens only on IPv4
- 5. A firewall rejects the attempt
- 6. A Kubernetes Service has no usable endpoints
- Reproduce and verify
- Related
TL;DR: The TCP connection attempt was rejected before HTTP could start. Check the exact address and port with
curl -v, then find the listener withss -ltnporlsof. For localhost, test IPv4 and IPv6 separately. For Docker, check both the app’s bind address inside the container and the host-to-container port mapping. A running process is not proof that the port is listening.
What it means
Chrome shows ERR_CONNECTION_REFUSED, with localhost refused to connect. when the hostname is localhost. Chromium’s error definitions distinguish a refused connection attempt from a reset connection. No HTTP response exists for the failed attempt, so changing request headers or CORS policy will not open the port.
The failure can appear in other clients as well. This curl output was captured against a closed local port with curl 8.7.1 on macOS:
curl: (7) Failed to connect to localhost port 3000 after 0 ms: Couldn't connect to server
The wording changed. curl 8.22.0, the current release checked for this guide, formats the destination with a colon and uses “Could not” in its connection failure source and error strings. Illustrative output for the same direct connection:
curl: (7) Failed to connect to localhost:3000 after 0 ms: Could not connect to server
Elapsed milliseconds vary. Exit code 7 means curl could not connect; use verbose output to identify the underlying failure rather than treating every code 7 as a refusal.
These Node messages were captured with Node 26.10.0, explicitly connecting to each loopback address on a closed port:
connect ECONNREFUSED 127.0.0.1:3000
connect ECONNREFUSED ::1:3000
The address matters: 127.0.0.1 is IPv4 loopback; ::1 is IPv6 loopback. A listener on one is not a listener on the other.
Refused, timeout and reset
- Refused: establishment gets an active rejection. A TCP SYN answered with RST is the usual closed-port case. RFC 9293 Section 3.10.7.1 specifies a reset response when no connection state exists for an incoming segment. A firewall can reject the attempt too.
- Connection timeout: establishment does not finish before the client’s deadline. Silent packet drops, a missing return path or an unresponsive destination can cause this. A timeout later in an established connection is a separate diagnosis.
- Reset: the connection is aborted with a TCP RST, potentially during TLS or while HTTP data is moving. Follow ERR_CONNECTION_RESET if the connection was established and then broken.
An immediate failure is a useful clue, not a guarantee that the application itself rejected you. Identify the destination and the rejecting layer.
Who sent it?
Run these on the machine where the client fails:
curl --noproxy '*' -v --connect-timeout 3 http://localhost:3000/
nc -vz 127.0.0.1 3000
nc -vz ::1 3000
Look at curl’s Trying addresses and whether it ever prints Connected to. --noproxy '*' makes this a direct probe rather than a test through an environment-configured proxy. nc -vz checks the TCP port without making an HTTP request.
Then inspect the server, inside the relevant container or VM if necessary:
# Linux: TCP listeners, numeric addresses/ports and owning processes
sudo ss -ltnp
# macOS, or Linux with lsof installed
sudo lsof -iTCP -sTCP:LISTEN -nP
Match the port and bind address. 127.0.0.1:3000 accepts local IPv4 connections; 0.0.0.0:3000 listens on all local IPv4 interfaces. An IPv6 wildcard :::3000 can also accept IPv4 on some systems, but that depends on the platform and socket configuration. Test both families.
Fix it, in order of evidence
1. Nothing is listening on the requested port
If the listener list has no matching address and port, start the service and inspect its startup output. Check whether it selected a different port or failed before binding. Use the port the server actually opened, not the port assumed by the frontend configuration.
For a crash-looping service, inspect the failed process rather than repeatedly refreshing the browser:
# Replace my-app with the systemd unit
systemctl status my-app
journalctl -u my-app -n 100 --no-pager
# Containers, including those that exited
docker ps -a
docker logs --tail 100 my-app
# Kubernetes: previous container instance's logs
kubectl -n web describe pod POD_NAME
kubectl -n web logs POD_NAME --previous
A process that crashes before binding leaves no listener. During a restart loop, the listener can appear and disappear. Fix the startup exception, failed dependency or container termination shown in the logs.
2. The app is bound to loopback inside Docker or a VM
localhost refers to the machine or network namespace making the connection. Your browser’s localhost is not the container’s localhost, and a second container’s localhost is not the first container.
For a bridge-networked container reached through a published port, bind the app to 0.0.0.0 inside the container. For example, a Node HTTP server:
const http = require('node:http')
http.createServer((req, res) => res.end('ok\n')).listen(3000, '0.0.0.0')
Publish that container port on the host:
docker run --name my-app -p 127.0.0.1:8080:3000 my-app-image
curl --noproxy '*' -v http://127.0.0.1:8080/
Here the app listens on container port 3000 and the browser connects to host port 8080. Binding the app to all container IPv4 interfaces is separate from deciding where Docker publishes the host port. Docker’s mapping documentation confirms that the explicit host 127.0.0.1 binding keeps this mapping local to the host.
For a VM, bind to 0.0.0.0 or its reachable interface and connect to the guest’s address, or configure the VM’s NAT forwarding. Publishing a Docker port cannot fix an application listening only on container loopback.
3. The port mapping points at the wrong container port
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
Illustrative PORTS values:
127.0.0.1:8080->3000/tcp
3000/tcp
The first publishes host 8080 to container 3000. The second lists a container port without a host mapping. EXPOSE alone does not publish a port. Compare the right side of the arrow with the listener inside the container; compare the left side with the URL you opened.
4. Localhost chose IPv6, but the server listens only on IPv4
Test each family and inspect Node’s resolver output:
curl --noproxy '*' -4 -v http://localhost:3000/
curl --noproxy '*' -6 -v http://localhost:3000/
node -e 'require("node:dns").lookup("localhost", {all:true}, console.log)'
Node 17.0.0 changed the default lookup ordering to verbatim: keep resolver order instead of moving IPv4 first. If ::1 comes first and the client tries only that address, an IPv4-only server can fail. --dns-result-order=ipv4first can diagnose that case:
node --dns-result-order=ipv4first client.cjs
autoSelectFamily became enabled by default in Node 20.0.0 and 18.18.0. It tries resolved IPv6 and IPv4 addresses until one succeeds; if all fail, it emits an AggregateError. Explicit family or localAddress settings disable that selection. Check the client’s options: DNS order alone does not establish the cause on current Node. A dns.setDefaultResultOrder() call also takes precedence over the CLI ordering flag.
Use an explicit loopback address for a client meant to use one family, or make the server listen on the required families. Retest both rather than assuming an IPv6 wildcard behaves identically on every OS.
5. A firewall rejects the attempt
Netfilter distinguishes reject from drop. Reject sends an error response; reject with tcp reset can make an initial connection look refused. Drop silently discards the packet, which can leave the client waiting until a timeout. Other rejection types can produce different OS errors.
Compare a local probe on the server with one from the failing client. If the listener exists and the local probe works, inspect firewall rules and counters along the external path. Do not equate refusal with proof that no process exists; an intervening device can reject traffic to a healthy listener.
6. A Kubernetes Service has no usable endpoints
kubectl -n web get service my-app -o yaml
kubectl -n web get endpoints my-app
kubectl -n web get endpointslices -l kubernetes.io/service-name=my-app -o yaml
kubectl -n web get pods -o wide --show-labels
Check the Service selector against Pod labels, Pod readiness, and targetPort against the app’s actual listener. kubectl get endpoints is useful on older clusters; use EndpointSlices as well, as the current Kubernetes debugging guide does. Inspect endpoint readiness, not just whether an address is listed.
In iptables kube-proxy, no endpoints at all produces REJECT rules. No local endpoints under a Local traffic policy can instead produce DROP rules. Other cluster networking implementations can differ. Inspect endpoint state before inferring the cause from “refused” versus “timeout”.
Reproduce and verify
Repeat the same client-side probe after the fix. First prove TCP connects, then prove the app answers:
nc -vz 127.0.0.1 3000
curl --noproxy '*' -v http://127.0.0.1:3000/
For Docker, use the published host port. For Kubernetes, probe the Service from the same network as the failing client and check its endpoints again. A TCP connection followed by an HTTP 404 or 500 proves refusal is resolved; the application response then needs its own diagnosis.
Related
- ERR_CONNECTION_RESET: the connection is aborted during TLS or data transfer.
- nginx 502 Bad Gateway: a reverse proxy cannot connect to its upstream.
- ERR_CONNECTION_TIMED_OUT: no reply at all, from silent firewall drops, stale addresses, broken IPv6 or a full listen backlog.
- DNS_PROBE_FINISHED_NXDOMAIN: the name never resolved, so no connection was attempted.
Frequently asked questions
What does localhost refused to connect mean?
The connection attempt to an address and port on your own machine was rejected. Check that the application is running and listening on the exact port and address family the client uses.
Why does 127.0.0.1 work while localhost fails in Node.js?
Localhost can resolve to IPv6 ::1 while the server only listens on IPv4 127.0.0.1. Node 17 changed DNS lookup ordering to verbatim, while Node 20 and Node 18.18 enabled family autoselection by default. Check the actual runtime and connection options before attributing a failure to ordering alone.
Why does my Docker container work internally but refuse host connections?
The app may listen only on the container loopback address, or its port may not be published to the expected host port. Bind the app to 0.0.0.0 inside the container and publish the correct container port.
How is connection refused different from a timeout?
Refused means the connection attempt received an active rejection. A connection timeout means establishment did not complete before the deadline, which can happen when packets or replies are dropped.
Can a Kubernetes Service exist and still refuse connections?
Yes. A Service can have no usable backend endpoints because its selector matches no Pods or its Pods are not ready. The iptables kube-proxy implementation installs REJECT rules when there are no endpoints at all; other networking implementations can behave differently.
Sources
- MDN: Connection management in HTTP/1.xdeveloper.mozilla.org
- RFC 9293: TCP reset and connection refusalrfc-editor.org
- Chromium source: connection error definitionsgithub.com
- Chromium source: browser error page stringsgithub.com
- curl 8.22.0 source: connection failure formattinggithub.com
- curl 8.22.0 source: error stringsgithub.com
- curl: command-line options and exit codescurl.se
- Node.js: DNS result orderingnodejs.org
- Node.js: socket connection family autoselectionnodejs.org
- Docker: port publishing and mappingdocs.docker.com
- Netfilter: nftables reject statementnetfilter.org
- Kubernetes: debug Serviceskubernetes.io
- Kubernetes source: kube-proxy rules for Services without endpointsgithub.com
- iproute2: ss manualgithub.com
- lsof: command manualgithub.com
- OpenBSD: nc manualman.openbsd.org
Related
ERR_CONNECTION_RESET and ECONNRESET: Find the RST
Fix ERR_CONNECTION_RESET, ECONNRESET and curl (56) Connection reset by peer: keep-alive races, body limits, firewalls, crashes. Find who sent the RST.
nginx 502 Bad Gateway: Causes and Fixes by Error Log
Fix nginx 502 Bad Gateway by matching the error log: connection refused, prematurely closed connection, php-fpm socket permissions, too big header, keepalive.
ERR_CONNECTION_TIMED_OUT: Trace TCP Connection Failures
Fix ERR_CONNECTION_TIMED_OUT by tracing SYN packets, checking firewalls and stale DNS, testing IPv6, and separating backlog pressure from MTU stalls.
ERR_EMPTY_RESPONSE: Find Who Closed the Connection
Debug ERR_EMPTY_RESPONSE and curl empty replies by checking app crashes, wrong ports, nginx 444, Docker listeners, keep-alive races and request framing.