Article View

Scroll down to read the full article.

Node.js DNS Failures in Alpine: When `getaddrinfo` Gets You Paged at 3 AM

calendar_month July 21, 2026 |
Quick Summary: Fix intermittent Node.js ENOTFOUND/EAI_AGAIN DNS errors in Alpine Linux containers (Node.js <16, Alpine <3.13) by forcing IPv4 resolution. Essenti...

Node.js DNS Failures in Alpine: When getaddrinfo Gets You Paged at 3 AM

Alright, listen up. We've all been there: a critical Node.js service, humming along, suddenly starts spewing ENOTFOUND or EAI_AGAIN errors. Your dashboards are red. PagerDuty is screaming. You ssh into the container, run nslookup google.com, and it resolves perfectly. Other containers on the same host are fine. Your network guy swears it's not the firewall. The dev team is blaming infrastructure. You're losing your mind.

This isn't your average "DNS misconfiguration." This is a highly specific, insidious bug that manifests as intermittent DNS resolution failures for external services, particularly in Node.js applications running on Alpine Linux within containerized environments. It’s the kind of problem that makes you question your life choices at 3 AM. Trust me, I’ve had my share of those nights – enough to write an entire article on Architectural Scales: Surviving 3 AM Pager Duty at FAANG. This particular issue is a textbook case.

The Symptoms: Intermittent, Maddening, and Misleading

You’ll see this primarily in Node.js applications attempting to connect to external HTTP/S endpoints, databases, or third-party APIs. The errors are sporadic. Sometimes it works for hours, then fails for a few requests, then recovers. No pattern. No clear trigger. Logs show:

  • Error: getaddrinfo ENOTFOUND api.external-service.com
  • Error: getaddrinfo EAI_AGAIN api.external-service.com

What makes it infuriating is that all standard network diagnostics inside the affected container pass with flying colors. ping, curl, wget, dig, nslookup – they all resolve the problematic hostname without issue. This leads to endless, fruitless chases down the rabbit hole of network policies, DNS server health, and firewall rules.

Affected Environments: The Unholy Trinity

This problem is a nasty cocktail of specific software versions interacting poorly. If your setup resembles this, pay attention.

Component Versions Where Error Triggers Notes
Operating System (Base Container) Alpine Linux <= 3.12 (e.g., 3.9, 3.10, 3.11, 3.12) Specifically due to musl libc resolver behavior.
Node.js Runtime Node.js <= 14.x (especially 12.x, 14.x) Later Node.js versions (16.x+, 18.x+) incorporate internal fixes or different default resolver behaviors.
Kernel (Host OS) Linux Kernel < 4.19 (or specific 4.x patches) Older kernels can mishandle AF_UNSPEC requests for IPv6 when no IPv6 is configured.
Network Configuration Container is IPv4-only, but DNS server may return AAAA records. Key ingredient: the Node.js resolver tries IPv6 first, even if not available.

The Endless Debugging Loop

You've probably tried:

  • Restarting the pod/container.
  • Bouncing the host.
  • Checking /etc/resolv.conf (it's usually fine, pointing to Kube-DNS).
  • Running tcpdump to observe DNS queries (they often don't even leave the container when the error occurs).
  • Updating your Node.js dependencies (irrelevant).
  • Blaming the cloud provider (sometimes justified, but not this time).
A digital ghost in a machine
Visual representation

The Root Cause: An AF_UNSPEC IPv6 Fiasco with musl and Node.js

Here’s the deal: Node.js, by default, uses the operating system's getaddrinfo function for DNS resolution. When you call getaddrinfo without specifying an address family (i.e., you use AF_UNSPEC, which means "give me IPv4 or IPv6"), Node.js typically prefers IPv6. It tries to resolve an AAAA (IPv6) record first, then falls back to A (IPv4).

The problem arises from the interaction of three factors:

  1. Alpine's musl libc Resolver: Unlike glibc, musl's resolver has a simpler, sometimes less robust, handling of AF_UNSPEC. Specifically, in older versions, if it makes an IPv6 query (AAAA record) and the DNS server responds with NODATA (meaning "no AAAA record for this hostname"), and the underlying network stack is IPv4-only (or IPv6 is disabled/unreachable), musl can sometimes incorrectly interpret this as a fatal resolution failure for all address families, not just IPv6. It essentially gives up on trying IPv4.
  2. Node.js's Default Preference for IPv6: Because Node.js (in these older versions) defaults to AF_UNSPEC and internally prioritizes IPv6, it hits this musl bug first.
  3. Older Linux Kernel Behavior: Compounding this, some older Linux kernel versions (particularly when a container has no IPv6 configured) can be slow or inconsistent in how they return errors for IPv6-related system calls, especially when an AF_UNSPEC request implicitly probes for IPv6. This delay or faulty negative response can push musl into its failure state.

So, you get a quick, "nope, no AAAA," musl throws up its hands, and Node.js never even tries for an A record. The result? ENOTFOUND, despite a perfectly resolvable IPv4 address being available.

The Fix: Force Node.js to IPv4 Resolution

The easiest, most immediate workaround is to tell Node.js to explicitly prefer IPv4 for DNS lookups. You can do this by setting an environment variable before your Node.js application starts. This forces getaddrinfo to request A records first, bypassing the problematic IPv6 path.


NODE_OPTIONS="--dns-result-order=ipv4first" node your_app.js

Or, if you’re using Kubernetes or Docker Compose:


# For Kubernetes Deployment manifest (spec.template.spec.containers[].env)
- name: NODE_OPTIONS
  value: "--dns-result-order=ipv4first"

# For Dockerfile (before `CMD` or `ENTRYPOINT`)
ENV NODE_OPTIONS="--dns-result-order=ipv4first"

# For Docker Compose
environment:
  - NODE_OPTIONS="--dns-result-order=ipv4first"

This setting instructs Node.js to try IPv4 DNS resolution first. If it succeeds, it uses that. If it fails, it then tries IPv6. For IPv4-only environments, this completely sidesteps the musl/IPv6 interaction bug.

Why This Works (and Why It's a Band-Aid)

This works because you're explicitly telling Node.js to change its internal preference, ensuring that it successfully resolves the A record before the problematic IPv6 path in musl can misbehave. It's an effective workaround for environments where you can't immediately upgrade the OS or Node.js runtime.

However, it's a band-aid. The true fix involves:

  • Upgrading Alpine Linux: Newer Alpine versions (3.13+) have updated musl libc, which includes fixes for this specific resolver behavior.
  • Upgrading Node.js: Node.js 16.x and particularly 18.x+ have improved internal DNS resolver logic, which makes them less susceptible to these underlying OS-level quirks.
  • Upgrading Kernel: Ensuring your host OS runs a recent Linux kernel (4.19+ or 5.x+) helps, as kernel-level IPv6 handling for AF_UNSPEC requests is more robust.
Tangled knot of network cables intertwined with a microscopic view of a CPU core
Visual representation

While this issue is frustratingly specific, remember that understanding how low-level components like libc resolvers interact with your runtime is crucial for deep systems debugging. It's not always about your application code. Sometimes, it's about the very foundations it runs on. For more insights on leveraging local resources for such deep dives, consider checking out articles like Ollama Unleashed: Local LLMs for the Production Grunt for powerful diagnostic capabilities right on your machine.

Don't Get Paged for This Again

You’ve seen the symptoms, understood the obscure root cause, and now you have a direct fix. Implement NODE_OPTIONS="--dns-result-order=ipv4first" in your affected deployments. Then, make a plan to upgrade your base images and Node.js runtimes. Save yourself the future headaches, the 3 AM calls, and the endless debugging loops. Go grab a coffee, you earned it.

Discussion

Comments

Read Next