Article View

Scroll down to read the full article.

The Phantom `getaddrinfo` ENOTFOUND: Node.js, Alpine, and the Musl DNS KeepAlive Trap

calendar_month July 25, 2026 |
Quick Summary: Battling intermittent Node.js getaddrinfo ENOTFOUND on Alpine? Discover how musl libc, keepAlive agents, and specific resolv.conf options conspire...

The Phantom getaddrinfo ENOTFOUND: Node.js, Alpine, and the Musl DNS KeepAlive Trap

Alright, listen up. If you've been tearing your hair out over seemingly random getaddrinfo ENOTFOUND errors in your Node.js applications running inside Docker containers on Alpine Linux, you're not alone. And no, it's not always a network issue, nor is your DNS server failing. This is one of those deeply frustrating, obscure problems that makes you question your life choices as an SRE. It's subtle, it's insidious, and it loves to hide in plain sight, mocking your extensive network troubleshooting skills.

You'll see it: an otherwise perfectly healthy service, making external API calls, suddenly spitting out EAI_AGAIN or ENOTFOUND. Then, moments later, it works fine without any intervention. It happens mostly after periods of low activity, or under very specific, transient load profiles that trigger connection reuse. Your first instinct is to blame the network. You spend hours tracing packets, pinging, digging, and curling from inside the container – everything looks golden, every DNS query resolves perfectly. Yet, your application logs tell a devastatingly different story: crucial external calls are failing. This, my friends, is the musl libc DNS resolver playing cruel tricks with your Node.js keepAlive agent.

A complex
Visual representation

The Scenario

Consider a typical Node.js microservice. It uses popular HTTP client libraries like axios or node-fetch, and you've wisely configured an http.Agent with keepAlive: true for efficiency. This is standard practice to reduce TCP handshake overhead for repeated connections to the same host. This service runs in a Docker container, often based on a lightweight Alpine image for its minimal footprint and faster build times. Everything seems perfectly optimized for performance and resource usage. Then, BOOM. Intermittent DNS resolution failures. Not for all external calls, mind you, just some, seemingly at random, making it maddeningly difficult to reproduce consistently.

Here’s where this specific flavor of hell thrives:

Component Version (Triggers Error) Version (Works Fine) Notes
Operating System Alpine Linux 3.14 - 3.19 (or newer with similar musl behavior) Debian 11+, Ubuntu 20.04+ The key difference is musl libc (Alpine) vs. glibc (Debian/Ubuntu) resolver implementations.
Node.js Runtime 16.x, 18.x, 20.x (any version effectively utilizing http.Agent) N/A Node.js's http.Agent is the mechanism that exposes the underlying libc issue, not the root cause itself.
HTTP Client Library Any library utilizing Node.js's native http/https module with keepAlive: true (e.g., Axios, Node-Fetch, Got) N/A Explicitly enabling keepAlive: true on an http.Agent is the critical configuration for this problem to surface.
DNS Configuration /etc/resolv.conf with aggressive options timeout:1 attempts:1 (or similar, overly strict settings) Default resolv.conf on glibc-based systems, or more forgiving timeout/attempts values. This configuration forces musl libc's resolver into an unforgiving, error-prone mode during transient network events.

The Root Cause

The problem lies in a nefarious interaction between Node.js's http.Agent, Alpine Linux's musl libc, and overly aggressive /etc/resolv.conf DNS options. Node.js's http.Agent is designed to reuse TCP connections. When keepAlive: true, it holds onto sockets after a request completes, hoping to reuse them for subsequent requests to the same host, which is a sound optimization.

Here's the rub: even with keepAlive, Node.js might perform implicit hostname re-resolution or validation checks, especially when a connection has been idle for some time, or if the underlying OS indicates a connection issue that requires a fresh lookup. On glibc-based systems (like Debian or Ubuntu), the DNS resolver is generally quite robust. It has more forgiving default timeouts and retries for network-level issues during resolution, intelligently cycling through configured nameservers and waiting patiently for responses.

Musl libc, used by Alpine, is renowned for its small size and speed. However, it's often less feature-rich and, critically, behaves differently under certain network and configuration conditions. When you combine musl libc with an aggressive /etc/resolv.conf entry like options timeout:1 attempts:1, you create a perfect storm. This tells the musl resolver to give up after a mere 1 second and only make 1 attempt per nameserver before failing. If the very first DNS query experiences even a flicker of network latency, packet loss, or a momentary load spike on the DNS server, musl will immediately and harshly fail with ENOTFOUND. Glibc, by contrast, would typically retry, wait longer, or cycle through other nameservers more gracefully before giving up and propagating an error. This is a common pitfall when attempting to optimize DNS resolution aggressively in containerized environments, often leading to instability. It's a subtle architectural flaw that frequently gets overlooked in favor of perceived efficiency gains.

The keepAlive agent exacerbates this because the error only surfaces when an existing connection is *reused* or implicitly validated, making it look like a network issue with the *target host* rather than a transient DNS resolution failure originating from your own container. It's not the target server that's down; it's your resolver having a tantrum, giving up far too soon. This often reminds me of the phantom Postgres Client Released error – another maddening case where Node.js's internal workings interact poorly with external system quirks, demanding a deep dive into underlying OS behavior.

A complex circuit board with intricate
Visual representation

The Fix: Stop Being So Aggressive With DNS

The solution is brutally simple once you understand the problem: stop telling musl libc to be so damn impatient. You need to provide more sensible, robust DNS resolution parameters to your Alpine containers. The easiest and most reliable way to do this is to explicitly set a more forgiving timeout and attempts in your container's /etc/resolv.conf. Docker, Kubernetes, and other orchestrators usually manage this through their own DNS configurations, but you can, and often *should*, override it at the container level.

For Docker, you can often achieve this by ensuring your Docker daemon's DNS configuration is sane, or by explicitly mounting a custom resolv.conf. For Kubernetes, you'd typically define this in your Pod's dnsConfig. But for an immediate, copy-pasteable container-level override that works in many scenarios within your Dockerfile or an entrypoint script, use this:


RUN echo "options timeout:2 attempts:5" >> /etc/resolv.conf

This command appends (or creates if it doesn't exist) the options timeout:2 attempts:5 line to /etc/resolv.conf. This tells the musl resolver to wait 2 seconds before timing out and to make up to 5 attempts per nameserver. This significantly increases its resilience against transient network hiccups or overloaded DNS servers, bringing its behavior closer to what you'd expect from glibc. It's a small change, but it makes a world of difference in stability.

You might also consider running a local DNS caching resolver like unbound or dnsmasq within your cluster or even within specific pods if the application makes an extremely high volume of external DNS queries. This adds another layer of caching and retry logic, further insulating your application from external DNS volatility and improving overall system stability. For applications operating at FAANG-level scale and entropy, robust DNS resolution and caching are non-negotiable architectural considerations, not just minor configurations.

Final Thoughts

This issue highlights a critical lesson in distributed systems: seemingly innocuous configuration choices, especially those related to fundamental network services like DNS, can have profound, cascading effects when interacting with different underlying system components. Musl libc is great for small container images, but its network stack behavior isn't always a 1:1 functional match with glibc. Always be skeptical of intermittent network-related errors, particularly in containerized environments where the 'host' OS is often stripped down. The fix is often less about 'fixing the network' and more about understanding the nuanced interactions of your application's runtime with the host's underlying system libraries and configured resolvers. Good luck out there, and may your DNS resolutions always be swift, successful, and retry-resilient.

Discussion

Comments

Read Next