Node.js · beginner

Node.js ECONNREFUSED: separate a dead listener from a blocked route

Distinguishing a Node.js ECONNREFUSED that points to a dead local listener from one that points to a blocked upstream route, using socket-level evidence rather than guesswork. The guide frames ECONNREFUSED as an operating system response (no listener accepted the SYN), then walks through the two most common causes: a target process that is down or bound elsewhere, and a route or firewall that silently drops traffic so the client never receives a RST.

The symptoms

  • Node.js application logs a `Error: connect ECONNREFUSED <address>:<port>` error string originating from `net.connect` or `http.request` with a target that the team believes to be reachable.
  • The error appears immediately rather than after a long timeout, indicating the kernel returned a TCP RST instead of an unreachable signal.
  • Re-running the same client call from the same host reproduces the error deterministically, while the same call from a different host or network succeeds.
  • Application health checks depending on the affected destination flap from healthy to unhealthy on the same cadence as process restarts or network changes.

Likely causes

  • Target Node.js server process is not running, has crashed, or has exited during reload, leaving the TCP port without an accepting socket and the kernel responding with RST.
  • Target server is bound to a specific interface or loopback address, while the client resolves the hostname to a different IP, producing a connection to an address that has no listener.
  • Upstream proxy, load balancer, or sidecar is configured but its listener is down, so the address the application dials has no live socket even though the address itself is routable.
  • Security group, host firewall, or container network policy silently drops SYNs without RST, masking a route or ACL problem as an apparent ECONNREFUSED to the client.
  • DNS resolution returns a healthy-looking record but the resolved IP belongs to a decommissioned host or a peer in a failover pair that is currently inactive.

First ten minutes

  1. 01Capture the exact target the client dialed: the destination IP, port, and the error code returned by the Node.js `Error` object, recorded from the application logs.
  2. 02Confirm the failure is reproducible from the same host by issuing the same `net.connect` or `http.request` programmatically, and confirm it succeeds from a peer host that is known to reach the service.
  3. 03On the target host, list processes and verify the server process is alive, then list listening sockets for the target port and the binding address; absence of a listener is the dead-listener signal.
  4. 04From the client host, resolve the destination hostname and compare the resolved IP against the address the server binds to; mismatch indicates a wrong-target problem rather than a network problem.
  5. 05Run a low-level probe from the client host to the same IP and port to observe whether the kernel receives a RST (dead listener) or no response at all (blocked route), then classify before changing anything.

Evidence to collect

  • Full Node.js error object, including `code` (must be `ECONNREFUSED`), `address`, `port`, `syscall`, and stack trace pointing to `net.connect` or `http.request`.
  • Output of a socket listing on the target host for the relevant port, showing the listening process, its PID, and the bound address family and address.
  • DNS resolution result from the client host for the hostname used by the application, including TTL and record type.
  • Outcome of a TCP probe from the client host to the destination IP and port, classified as immediate RST, timeout, or other, with timestamp.
  • Recent deployment, restart, or configuration change timestamps for the target process and any proxy, sidecar, or firewall in the path.

Where to look

  • Application logs at the boundary between the Node.js client code and the kernel, where `net.connect` or `http.request` raise the error with `code: ECONNREFUSED`.
  • The target host's listening socket table for the affected port, to verify a process owns that port and to read its bound address.
  • The DNS resolver used by the client, especially if the client uses a service-discovery or service-mesh name rather than a literal IP.
  • Network boundary in front of the target, including cloud security groups, host firewall rules, container network policies, and any sidecar or proxy in the request path.
  • Process supervisor records for the target service to correlate crashes, restarts, or rollbacks with the first ECONNREFUSED timestamp.

Diagnostic steps

  1. 01Read the Node.js error object and confirm `err.code === 'ECONNREFUSED'`; a different code (such as `ETIMEDOUT`, `EHOSTUNREACH`, or `ENOTFOUND`) means the failure is not a dead listener and the rest of this guide does not apply.
  2. 02Note `err.address` and `err.port`; these are the address and port the kernel attempted to connect to, which is the only authoritative target for further diagnostics.
  3. 03On the destination host, run a process listing filtered to the service, then a listening-socket listing filtered to that port; the presence of a matching socket rules out the dead-listener class for that specific address.
  4. 04Compare the bound address of the listening socket to `err.address`; if the server binds only to loopback while the client reaches an external IP, the cause is wrong-target, not network reachability.
  5. 05Run a TCP probe from the client host to `err.address:err.port` and observe the response: an immediate RST indicates a host that answered but had no listener (dead-listener or wrong-target); a timeout with no response indicates a silent drop (route or firewall).
  6. 06If the probe times out, inspect the network path layer by layer: host firewall counters, security group flow logs, and any NAT or proxy in between, to localize where SYNs stop being acknowledged.
  7. 07If the probe receives RST but a listener exists, the client is reaching a different endpoint than expected; re-check DNS, service discovery, and any hard-coded endpoints in configuration.

Common mistakes

  • Treating ECONNREFUSED as a generic connectivity error and restarting the client or the network, when the real cause is a server bound to the wrong interface or stopped altogether.
  • Assuming a successful HTTP probe from a workstation proves the service is healthy, when the client process resolves a different hostname or connects from a different source IP that the firewall does not allow.
  • Adding retries or longer timeouts to mask a deterministic ECONNREFUSED, which converts a clear, immediate failure into a slow, expensive one without addressing the root cause.
  • Editing configuration files speculatively without first capturing `err.address`, `err.port`, and the listening-socket state, so subsequent changes cannot be correlated with the original failure.
  • Confusing a route or firewall drop (no RST, timeout) with ECONNREFUSED; only the dead-listener class returns RST, and conflating the two leads to incorrect remediation steps.

Safe fixes

  • If the target process is not running, restart it through the existing supervisor and only after confirming the prior process exited cleanly; verify the listening socket reappears on the expected address and port.
  • If the target process is bound to loopback while the client expects an external IP, change the bind configuration to the appropriate interface, restart the service, and re-validate that the listening socket now matches the client's target.
  • If DNS resolves to a decommissioned or peer IP, correct the record or override in the client configuration to point at the live endpoint, and document the change to prevent recurrence.
  • If a sidecar or proxy that the application depends on is down, restart that component first and confirm its listener is up before restarting the application, so the application does not flap on the same root cause.
  • If a firewall or security group is silently dropping SYNs, add a narrowly scoped rule permitting the client's source IP to the destination port, and verify with a TCP probe that the kernel now receives a RST or a SYN-ACK rather than silence.

Prove the fix

  1. 01Re-run the original Node.js client code path and confirm the error no longer appears, with the application's structured logs showing a successful connect or response from the target.
  2. 02From the same client host, run a TCP probe to the same IP and port and observe either a successful handshake or a clear refusal, both of which are deterministic outcomes distinct from a silent drop.
  3. 03On the target host, confirm the listening socket for the port is owned by the expected process and bound to the address the client now resolves to.
  4. 04Cause one controlled failure (stop the listener or block the route) and confirm the application logs the expected `ECONNREFUSED` or timeout signature, demonstrating the detection path still works.
  5. 05Capture before and after evidence in the same format (IP, port, error code, listening socket state) so the fix can be reviewed without re-running the original incident.

Prevention and next steps

  • Standardize the address the application dials and the address the server binds to in a shared configuration source, and lint configuration to reject mismatched interfaces.
  • Add a startup self-check that attempts a local TCP connect to the configured dependency address and fails fast with a clear error if no listener accepts the connection.
  • Export listening-socket state and `ECONNREFUSED` counts as structured metrics, with alerts on sustained non-zero refusal rates per dependency.
  • Track DNS record changes and service-discovery endpoint changes through version control so that a sudden switch to a decommissioned IP can be correlated with the first ECONNREFUSED.
  • Review firewall and security group rules whenever a new client host or CIDR is added, and document the expected outcome of a probe so silent drops are not mistaken for refusals.

Safe commands and checks

ss -ltnp 'sport = :<port>'  # on the target host, list processes listening on <port> with bound addresses; absence rules in the dead-listener class.
ps -ef | grep <service-name>  # on the target host, confirm the server process is alive and note its PID for further inspection.
getent hosts <hostname>  # on the client host, resolve the hostname the application uses and compare the result to the server's bound address.
node -e "require('net').connect(<port>, '<resolved-ip>').on('error', e => console.error(e.code, e.address, e.port))"  # reproduce the client-side connect from the same host and capture the kernel's response class.
timeout <seconds> bash -c 'cat </dev/tcp/<resolved-ip>/<port>'  # low-level TCP probe to classify immediate refusal vs. silent drop without using a local HTTP client.
journalctl -u <service-name> --since '<timestamp>'  # correlate process restarts or crashes with the first ECONNREFUSED timestamp captured from application logs.
node --trace-uncaught app.js  # when the failure occurs inside the application, capture a stack trace that points to the exact `net.connect` or `http.request` call site for evidence.