Quick Summary: Node.js freezes? Uncover how systemd-resolved and blocking DNS lookups stall your event loop. A veteran SRE's definitive guide to fixing this obsc...
You've got a Node.js service, humming along, then suddenly… silence. Not a crash. Not a high CPU spike. Just a complete freeze. Requests pile up. Your monitoring screams latency_high. The event loop is blocked, deadlocked, utterly unresponsive. It’s infuriating.
You restart the process. Everything's fine for a while, then BAM! Again. It’s intermittent, insidious, and impossible to debug with standard tools. You're losing hair, and your users are losing patience. This isn't a memory leak, it's not CPU thrashing. It's a phantom.
The Symptoms: Your App is a Zombie
- Your Node.js process is marked
S(sleeping) intop, or shows minimal CPU usage, despite high request backlog. - HTTP requests pile up; new connections aren't accepted or processed.
- Old requests hang indefinitely until client timeouts.
- No error logs from the application itself related to a crash.
- Heap dumps and CPU profiles are often useless, pointing to `poll` or `epoll_wait` as the bottleneck, but offering no 'why'.
- It feels like network issues, but other services on the same host are fine.
You've checked everything. Network connectivity, database health, upstream service latencies. All green. But your Node.js application is a brick. You've probably already wasted days chasing red herrings.
The Specific, Obscure Trigger Environments
This particular flavor of hell tends to manifest under these conditions. Not exclusively, but these are the prime suspects:
| Operating System / Kernel | systemd-resolved Version |
Node.js Version (LTS) | Notes |
|---|---|---|---|
| Ubuntu 20.04 LTS (Kernel 5.4.x) | 245-1/245.4-4ubuntu3 | 12.x, 14.x, 16.x | Common in containers; uses 127.0.0.53 stub by default. |
| CentOS 8 / RHEL 8 (Kernel 4.18.x) | 239-43.el8_2.4 | 12.x, 14.x, 16.x | Similar stub resolver behavior. |
| Debian 10 (Kernel 4.19.x) | 241-5~deb10u4 | 12.x, 14.x, 16.x | If systemd-resolved is enabled and misconfigured. |
It's not just about the versions, but the interaction. The subtle dance of a synchronous DNS call, a single-threaded event loop, and a flaky system resolver.
The Root Cause: systemd-resolved's Blocking Embrace
Here's the ugly truth: Your Node.js application is getting repeatedly blocked by a slow or failing DNS resolution, specifically via getaddrinfo. Node.js's built-in dns.lookup() function (used by default for hostname resolution in modules like http, https, and net) relies on the underlying operating system's getaddrinfo call from glibc. This call is synchronous and blocking.
On modern Linux distributions, systemd-resolved acts as a local DNS stub resolver, listening on 127.0.0.53. By default, your /etc/resolv.conf likely points to this stub. When your Node.js app tries to resolve a hostname, it queries 127.0.0.53. If systemd-resolved itself is struggling to reach its upstream DNS servers (due to network issues, misconfiguration, slow DNSSEC validation, or even rate limiting by external resolvers), that synchronous getaddrinfo call blocks. For the full DNS timeout. Usually 5 seconds. Per server. Multiplied by retries.
Since Node.js runs on a single event loop, a blocking getaddrinfo call stalls the entire application. Everything. No new requests. No existing request processing. Just dead air. It's subtle because systemd-resolved isn't crashing, just becoming unresponsive or slow to return results. For a related discussion on how systemd-resolved can sneakily kill your Node.js containers when the 127.0.0.53 stub is inaccessible, check out Unmasking the Ghost in the Machine: Node.js, Docker, and the systemd-resolved Stealth Freeze. This problem, however, is subtly different: your container can reach the stub, but the stub itself is choking.
The Fix: Sledgehammer and Scalpel (Mostly Sledgehammer)
The fastest, most reliable way to exorcise this ghost is to bypass systemd-resolved entirely for system-wide DNS resolution. This ensures your getaddrinfo calls go directly to reliable external DNS servers, rather than through a potentially flaky local stub.
Execute these commands on your host system (or within your container's entrypoint script if applicable, though modifying the host is generally more robust for this specific issue):
#!/bin/bash
# Stop and disable systemd-resolved to prevent it from managing /etc/resolv.conf
sudo systemctl stop systemd-resolved.service
sudo systemctl disable systemd-resolved.service
# Remove the symlink if it exists (common for systemd-resolved)
sudo unlink /etc/resolv.conf || true
# Create or overwrite /etc/resolv.conf with reliable, external DNS servers
# Using Google DNS for this example. Replace with your preferred stable resolvers.
echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf
echo "nameserver 8.8.4.4" | sudo tee -a /etc/resolv.conf
# Add critical options for fast failure and rotation
echo "options timeout:1 attempts:1 rotate" | sudo tee -a /etc/resolv.conf
# Verify the change
cat /etc/resolv.conf
# You may need to restart your Node.js application after this for changes to take full effect.
Why This Nuclear Option Works
You've just forcibly removed the problematic systemd-resolved stub resolver from the critical path for getaddrinfo calls. Now, your Node.js application's DNS lookups go directly to the configured external DNS servers (like 8.8.8.8 and 8.8.4.4), bypassing the internal bottleneck. The timeout:1 attempts:1 rotate options are crucial: they instruct the resolver to try each nameserver for only 1 second, attempt once, and rotate through the list. This means even if an external DNS server is slow, your getaddrinfo call won't block for more than a couple of seconds total, allowing your Node.js event loop to recover much faster.
This is a heavy-handed fix, but for this specific, insidious problem, it's often the most effective. It cuts the Gordian knot that systemd-resolved tied around your Node.js app's performance.
Preventing Future Headaches
While this fixes the immediate problem, consider the broader implications. Your application's reliance on external services means you need robust DNS. Don't just set it and forget it. Monitor your chosen upstream DNS resolvers. Ensure they are performant and reliable. For architectural considerations around such critical dependencies, you might find insights in Scalpel and Sledgehammer: Architecting Distributed Systems for FAANG-Scale Resilience. DNS is infrastructure. Treat it as such.
This is another reminder that infrastructure quirks can deeply impact application behavior, even in seemingly abstract environments like Node.js. Don't assume the underlying OS is always benign. Dig deep. Trust nothing.
Comments
Post a Comment