Node.js · advanced

Node.js address family error: distinguish IPv4 and IPv6 binding

This guide distinguishes Node.js address-family mismatches between IPv4-only and IPv6-only sockets, focusing on the failure mode where a listener or client requests an address family unavailable on the active host. It covers diagnostic boundaries, evidence-gathering steps, and conditional fixes anchored to Node's documented DNS, net, and error semantics. The intent is to prevent misdiagnosis between DNS resolution failures, dual-stack socket binding conflicts, and intentional family pinning in libraries such as net, http, https, dgram, and tls.

The symptoms

  • listen EADDRNOTAVAIL or EAFNOSUPPORT appears with a literal IPv6 address (:: or ::1) on a Node process bound to an IPv4-only interface via net.createServer or http.createServer.
  • Error: getaddrinfo ENOTFOUND paired with the rejected node version of an AAAA query when dual-stack resolution expected an A record, often surfaced from dns.lookup inside http.request.
  • getaddrinfo EAI_AGAIN entries repeated only for one address family while the other resolves successfully, indicating asymmetric resolver behavior between IPv4 and IPv6.
  • connect ECONNREFUSED or ETIMEDOUT traces that resolve a hostname to both A and AAAA records but only the wrong-family destination is reached because of a pinned family on the socket.
  • Cluster or container restarts report EADDRINUSE on an IPv6-mapped IPv4 address (::ffff:0.0.0.0) when the operating system has IPv6 disabled at the kernel level but Node still attempts dual-stack binding.
  • TLS handshake or HTTP upgrade failures cite "wrong family" inside OpenSSL callbacks when a custom lookup function returns only one record family despite dual-stack network reachability.

Likely causes

  • The host running the Node process has IPv6 disabled or link-local-only IPv6, yet the application code or environment variable hard-codes an IPv6 literal or family hint.
  • An explicit dns.lookup options object sets family: 6 (or 4) but the destination has no record of that family, so resolution yields ENOTFOUND even though the other family would succeed.
  • Library code such as http.request, net.connect, or third-party SDKs default to family 4 on older Node versions where AAAA records exist but A records are absent, blocking dual-stack resolution.
  • Container or VM network policy strips IPv6 routing while Node's auto-selection picks the IPv6 record first; the resolved address is unreachable from inside the namespace.
  • A reverse proxy or sidecar listens on an IPv4 wildcard while the upstream Node listener binds to IPv6-only, producing a route mismatch that surfaces as an address-family refusal at the proxy boundary.
  • node --dns-result-order=verbatim or NODE_OPTIONS affecting resolver behavior interacts with a pinned family so that AAAA results are tried before A and the wrong-family path is selected first.

First ten minutes

  1. 01Capture the failing command, the bound interface, and the literal address string from the error message; record whether the literal contains a colon character that marks an IPv6 address.
  2. 02Identify which API raised the error: net.Server.listen, dgram.bind, http.request, tls.connect, or dns.lookup, because each applies family defaults differently.
  3. 03Inspect Node's process flags and environment for --dns-result-order, --enable-source-maps, and any library-level default that selects family 4 or family 6 on older runtimes.
  4. 04Cross-check the host's actual capability: confirm whether the kernel exposes IPv6, whether the network interface has a global IPv6 address, and whether the routing table carries a default IPv6 route.
  5. 05Separate the two failure surfaces: outbound resolution failure (client-side family pinning) versus inbound binding failure (listener-side family mismatch).
  6. 06Decide whether to fix by enabling IPv6 on the host, switching to a family that the host supports, or supplying an explicit family hint to the resolution API.

Evidence to collect

  • The exact Node error code and message text, including whether the code is EADDRNOTAVAIL, EAFNOSUPPORT, ENOTFOUND, EAI_AGAIN, ECONNREFUSED, or ETIMEDOUT.
  • The literal address or hostname attached to the failing call, captured with surrounding stack frames and any error.code, error.syscall, and error.address properties.
  • Node version output from `node --version` and any active NODE_OPTIONS or `--dns-result-order` value visible in process metadata.
  • Host-side evidence of address-family support: presence of a global IPv6 address on the active interface and existence of a default IPv6 route in the routing table.
  • Resolver evidence: which record types (A, AAAA) the system's resolver returns for the failing hostname, and whether the result is consistent across multiple lookups.
  • Listener evidence: which address family the bound socket uses, the port in use, and whether the bind succeeded before the family-specific error appeared.

Where to look

  • Node's errors documentation page (nodejs.org/api/errors.html) for the canonical meaning of EADDRNOTAVAIL, EAFNOSUPPORT, ENOTFOUND, and EAI_AGAIN as system errors mapped from libuv.
  • The net module documentation for Server.listen semantics, including the IPv6 bracket form and how the OS interprets an unspecified address across families.
  • The dns module documentation for dns.lookup and dns.promises.lookup, particularly the family option, the verbatim ordering flag, and how custom lookup callbacks affect net and http consumers.
  • The http and https modules for how request inherits defaults from globalAgent and how a custom lookup function influences outbound address selection.
  • The dgram module documentation for UDP bind behavior on dual-stack sockets and the constraints that apply when IPv6 is not enabled at the OS layer.
  • Platform network configuration: interface address listings and routing tables on the host, container, or VM where the Node process is running, to verify IPv6 capability independently of Node.

Diagnostic steps

  1. 01Read the error code and address property: EADDRNOTAVAIL or EAFNOSUPPORT indicates the OS rejected the bind for that family; ENOTFOUND or EAI_AGAIN indicates resolver rejection rather than socket binding.
  2. 02Decide which surface failed first: a stack trace landing in net.Server.listen or dgram.bind implies binding; a trace in dns.lookup or http.request implies resolution.
  3. 03Check the Node version against documented behavior changes in the errors reference: EAI_AGAIN is the documented libuv mapping for getaddrinfo temporary failure, distinct from ENOTFOUND which signals no records.
  4. 04Verify host IPv6 capability by inspecting interface addresses and routes, independent of Node, so that any "fix" inside Node code is grounded in actual platform support.
  5. 05For resolver-driven failures, test whether the hostname resolves under both family 4 and family 6 individually, and whether the failure is symmetric or limited to one family.
  6. 06For listener-driven failures, attempt a minimal net.Server listen on each family with a placeholder port to determine which literal addresses the OS accepts on this host.
  7. 07Compare behavior under `--dns-result-order=verbatim` and `--dns-result-order=ipv4first` to confirm whether result ordering interacts with a family pin rather than causing the failure outright.
  8. 08Trace library defaults: many HTTP clients fall back to family 4 on older Node versions, which can mask dual-stack intent and route only one family even when both succeed.

Common mistakes

  • Treating ENOTFOUND as a DNS outage when the underlying cause is a family-specific lookup that misses because the hostname has only AAAA records and the call pinned family 4.
  • Forcing IPv4-only operation as a fix without verifying that the destination service is reachable on IPv4, which can convert a family-mismatch into a routing or firewall failure.
  • Switching to a literal IPv6 address in code while the host only has IPv4 configured, producing EADDRNOTAVAIL on the listener and re-introducing the original symptom under a new code.
  • Suppressing EAI_AGAIN with retries without isolating whether the resolver is returning no records or temporarily failing, which Node's documentation distinguishes through separate error codes.
  • Reading dual-stack comments that say ":: listens on both families" without confirming the host kernel actually supports IPv6, since the comment only holds when the OS provides the address.
  • Assuming a Node upgrade alone resolves the symptom: behavior changes in resolver ordering and default family are documented, but only matter if the application code interacts with them.

Safe fixes

  • If the host lacks IPv6 and the code binds a literal IPv6 address, switch the bind target to a family the host supports, then verify the listener accepts connections with a scoped net probe.
  • If the failure is on outbound resolution, pass an explicit family option to dns.lookup or the http/https request's lookup function, matching the family that the destination actually publishes in DNS.
  • If a custom lookup callback is returning the wrong family, normalize the callback to prefer the family supported by both endpoints, and surface the chosen family in structured logs.
  • If dual-stack is intended but the kernel disables IPv6, enable IPv6 on the host network configuration rather than masking the requirement inside Node, then re-run the bind probe.
  • If Node version-driven defaults are suspected, set the documented resolver flags such as --dns-result-order=ipv4first only after confirming the destination supports both families, to avoid asymmetric retries.
  • If a reverse proxy sits in front of Node, align the proxy's listen family with Node's listen family so the boundary does not silently drop one family of traffic at the proxy layer.

Prove the fix

  1. 01A net.Server bound with the corrected address literal accepts a single connection from a client using the same family, observable through the 'connection' event firing without a follow-up error event.
  2. 02An outbound http.request that previously returned ENOTFOUND now returns a response with a populated response.statusCode, and dns.lookup called with the explicit family option returns one or more addresses.
  3. 03Restarting the process with the corrected bind or lookup family produces no EADDRNOTAVAIL, EAFNOSUPPORT, ENOTFOUND, or EAI_AGAIN in the next capture window, while other unrelated errors remain visible if they exist.
  4. 04Resolver queries for the failing hostname return records of the requested family, and the chosen address is reachable from the Node process according to a documented connect or probe step that does not depend on loopback URLs.
  5. 05A second family-aware probe, repeated with a different placeholder port, behaves consistently across runs, demonstrating that the bind or resolution path is stable rather than timing-dependent.
  6. 06Documentation anchors: the Node errors reference continues to define the observed codes as documented libuv mappings, and the chosen fix aligns with the documented behavior of net, dns, http, or dgram.

Prevention and next steps

  • Centralize address-family selection in a single lookup helper so that every outbound call uses the same family policy, with explicit family arguments instead of implicit defaults.
  • Capture the resolved family alongside the chosen address in structured logs, so future incidents can distinguish family pinning from genuine DNS outages at a glance.
  • Document the host's IPv6 capability in deployment runbooks and fail fast at startup if a hard-coded bind target cannot be reached on the active family, rather than retrying silently.
  • Validate library defaults during upgrades: review release notes for resolver ordering, default family changes, and dual-stack behavior in net, http, and dns before rolling Node versions.
  • Align proxy, sidecar, and application listen families explicitly, and surface the active family through a readiness signal so operators can detect mismatches before traffic is affected.

Safe commands and checks

node --version
node -e "require('dns').lookup('example.com', { family: 4, all: true }, (e, a) => console.log(e || a))"
node -e "require('dns').lookup('example.com', { family: 6, all: true }, (e, a) => console.log(e || a))"
node --dns-result-order=ipv4first -e "console.log(require('dns').getDefaultResultOrder ? require('dns').getDefaultResultOrder() : 'n/a')"
node -e "const s=require('net').createServer(); s.on('error', e => console.log(e.code, e.address, e.port)); s.listen(0, '0.0.0.0', () => { console.log('ipv4', s.address()); s.close(); })"
node -e "const s=require('net').createServer(); s.on('error', e => console.log(e.code, e.address, e.port)); s.listen(0, '::', () => { console.log('dual', s.address()); s.close(); })"
node -e "console.log(Object.keys(require('os').networkInterfaces()).map(n => ({n, fam: require('os').networkInterfaces()[n].map(i => i.family)})))"
node -e "process.on('uncaughtException', e => console.log('code', e.code, 'syscall', e.syscall, 'address', e.address, 'port', e.port)); require('net').createServer().listen(0, '::', () => process.exit(0))"