Article View

Scroll down to read the full article.

Node.js DNS Hell: The 1ms getaddrinfo Stall That Killed Your Microservice

calendar_month August 15, 2026 |
Quick Summary: Debugging an obscure Node.js DNS resolution stall on Linux under high CPU. Learn the root cause and a critical fix for intermittent microservice t...

You’ve been there. The pager screams at 3 AM. A critical microservice is flapping, spewing connection timeouts like confetti at a bad wedding. Logs are useless: "ECONNREFUSED", "ETIMEDOUT", "getaddrinfo ENOTFOUND". You restart, it stabilises. Five minutes later, chaos erupts again. Sound familiar? Good. Because you’re about to fix that particular brand of hell.

This isn't about resource exhaustion. Not really. Not in the way you usually think. Your CPU isn't pegged at 100% consistently. Memory isn't swapped to oblivion. But those timeouts? They only happen under peak load, specifically when your Node.js application is doing some serious number-crunching or processing high volumes of requests. The moment things quiet down, poof, everything's fine.

We're talking about a beast of a problem, insidious and subtle. It’s a specific confluence of operating system kernel versions, glibc, and Node.js’s reliance on libuv for its asynchronous DNS lookups. Specifically, the dreaded getaddrinfo stall that becomes synchronous when it absolutely, positively shouldn't.

Here are the environments where this particular headache loves to manifest:

Operating System Kernel Version Range Node.js Version Range Symptoms
Ubuntu LTS (e.g., 20.04) 5.4.0-x to 5.8.x 14.17.x to 14.19.x, 16.13.x to 16.16.x Intermittent ECONNREFUSED/ETIMEDOUT on external service calls under high concurrent CPU load. DNS resolution errors.
Alpine Linux 5.4.x to 5.8.x 14.17.x to 14.19.x, 16.13.x to 16.16.x Similar to Ubuntu, potentially more aggressive due to musl libc, though observed less frequently.

You’ve likely checked everything. DNS servers are fine. Network connectivity is solid. Firewall rules? Pristine. Your code doesn't explicitly call synchronous DNS functions. You've upped uv_thread_pool_size, thinking it's a thread pool starvation issue. It helps, sometimes, but doesn't eliminate it. The problem persists, mocking your SRE prowess.

Tangled server rack wires illuminated by a single
Visual representation

The Root Cause

Alright, strap in. This is where it gets nasty. Node.js, via libuv, uses a thread pool to perform blocking operations, including DNS lookups through getaddrinfo. This is supposed to prevent the event loop from blocking. However, in specific glibc versions (2.31-2.33, common in older LTS distributions) running on Linux kernels from 5.4.x up to around 5.8.x, a subtle interaction causes chaos.

When getaddrinfo is asked to resolve AF_UNSPEC (meaning "give me IPv4 or IPv6, whatever you find") and the DNS server preferentially returns IPv6 records (which is increasingly common), or the system's network configuration prioritizes IPv6, a specific path within glibc's resolution logic can become extraordinarily slow. It’s not just slow; it can exhibit blocking behavior for hundreds of milliseconds.

Normally, this blocking would be contained within libuv's thread pool. But when your Node.js application is under heavy CPU-bound load – think complex data transformations, intensive cryptographic operations, or just a lot of concurrent real-time event pipeline processing – the Linux kernel's scheduler, particularly in the 5.4-5.8 range, can struggle with thread prioritization. This is especially true on systems with many cores where futex contention or specific CPU affinity settings can cause the libuv worker threads (which are system threads, after all) to be starved of CPU cycles or suffer excessive context switching overhead.

The result? The "asynchronous" getaddrinfo call, waiting for its turn in a busy thread pool that's itself contending with the main Node.js process for CPU time, can effectively become synchronous relative to the event loop. Hundreds of milliseconds of event loop blocking. This isn't theoretical; it impacts algorithmic trading latency in real-world scenarios. Your critical external API calls, waiting for DNS resolution, time out before they even get a chance to establish a connection.

This isn't a problem with your DNS servers. It's an internal process hang, exacerbated by external load and a fragile system-level interaction. It's a bug that's been largely patched in newer glibc and kernel versions, but if you’re stuck on specific older LTS builds, you’re in the hot seat.

A digital circuit board with a single
Visual representation

The Solution

The fix is a crude but effective surgical strike: bypass the problematic AF_UNSPEC resolution path altogether. Forcing Node.js to only attempt IPv4 lookups for external HTTP/HTTPS connections sidesteps the glibc bug where IPv6 resolution and kernel scheduling converge to create a blocking nightmare. You effectively cut off the "slow path" for DNS resolution.

Apply this globally if your environment permits (i.e., you don't need IPv6 for external connections, or internal services rely solely on IPv4). For Node.js applications making HTTP/HTTPS requests, you can modify the global agent options:


// Add this at the very top of your application's entry file (e.g., index.js or app.js)
// BEFORE any HTTP/HTTPS requests are made or agents are initialized.

// Standard HTTP agent
require('http').globalAgent.options.family = 4;
console.log('HTTP: Forced globalAgent to use IPv4 only.');

// Standard HTTPS agent
require('https').globalAgent.options.family = 4;
console.log('HTTPS: Forced globalAgent to use IPv4 only.');

// If you are using a custom agent or libraries like `axios`, `node-fetch`, etc.
// you might need to configure them specifically or ensure they respect globalAgent settings.
// For `axios` with Node.js, it typically uses the globalAgent.
// For `node-fetch`, you might need to pass `agent: new https.Agent({ family: 4 })` directly.

// Alternatively, and often complementary, you can set an environment variable:
// export NODE_OPTIONS='--dns-result-order=ipv4first'
// This influences dns.lookup() behavior more broadly,
// but the globalAgent setting is more direct for HTTP/HTTPS requests.

Restart your service. Immediately. Watch the connection timeouts vanish. The intermittent flapping will cease. Your pager will be silent. You’ll be able to sleep again. This isn't a long-term architectural fix; it's a critical production hotfix to stabilize systems running on vulnerable platforms. For true resilience, upgrade your OS, kernel, and Node.js versions. But for now, this will get you out of the fire.

This override ensures that getaddrinfo, when called by Node.js for outgoing HTTP/HTTPS connections, will explicitly request only IPv4 addresses. This bypasses the specific problematic glibc code path associated with AF_UNSPEC and IPv6 prioritization under kernel scheduling pressure. It's a pragmatic solution to a deeply annoying, obscure bug that can bring down high-throughput systems.

Go forth and fix your broken services. And maybe, just maybe, push for those system upgrades. Seriously.

Discussion

Comments

Read Next