Quick Summary: Solving intermittent Node.js ETIMEDOUT errors in Docker on CentOS 7 under CPU load. A deep dive into nf_conntrack, kernel timing, and sysctl fixes...
Alright, listen up. If you've spent weeks tearing your hair out over intermittent ETIMEDOUT errors in your Node.js application, running in Docker, specifically on older CentOS 7 hosts, under load, you're not alone. I've been there. It's infuriating. It looks like a network problem, acts like a network problem, but it’s a phantom, a ghost in the machine tied to CPU cgroup throttling and an archaic kernel interaction. Let's kill it.
You've checked your firewall rules. You've verified DNS. You've looked at application logic, resource limits, network latency, MTU sizes, everything. Still, random ETIMEDOUT errors plague your outbound HTTP/HTTPS connections. They only seem to hit when the application is truly busy, and your Docker host is under significant CPU pressure. You're thinking, "It's the network!" But every network engineer stares blankly. Because it isn't.
The symptoms are always the same: a sudden burst of outbound connection failures, all reporting ETIMEDOUT. It's not DNS resolving to the wrong IP, it's not a blocked port. The connection just… vanishes. Like it never even tried. Or worse, it times out after a painfully long wait. Then, when the load subsides, everything goes back to normal. Until the next peak.
You've probably already wasted days fiddling with Node.js keepAlive, increasing connection pools, bumping system-wide file descriptor limits. Stop. Those are band-aids on a gaping wound you haven't even seen yet.
Environments Where This Beast Lurks
This problem is a specific intersection of older kernel behavior and resource management. Here's where we've consistently observed it:
| Operating System | Docker Version | Node.js Version |
|---|---|---|
| CentOS 7.x (Kernel < 3.10.0-1062) | 19.03.x, 20.10.x | 14.x, 16.x, 18.x |
| RedHat Enterprise Linux 7.x (Kernel < 3.10.0-1062) | 19.03.x, 20.10.x | 14.x, 16.x, 18.x |
If you're on a newer kernel (like CentOS 8+, or recent Ubuntu/Debian), you're likely safe. The kernel fixes in later versions address the underlying timing issues.
The Root Cause
Here's the ugly truth: this is a nasty interaction between Docker's CPU cgroup throttling, older Linux kernel's nf_conntrack table management, and the system's ephemeral port allocation, specifically how it handles TIME_WAIT states. When your Docker container is CPU-throttled, the kernel's cgroup scheduler kicks in. On older CentOS 7 kernels, this throttling can introduce subtle timing discrepancies in how the network connection tracker (nf_conntrack) updates its state.
Specifically, when a Node.js process makes many short-lived outbound connections, they quickly cycle through various TCP states, including TIME_WAIT. While in TIME_WAIT, the connection (source IP, source port, dest IP, dest port, protocol) occupies an entry in the kernel's nf_conntrack table. The system also has mechanisms like tcp_tw_reuse and tcp_tw_recycle to manage these. However, with CPU throttling, the kernel's ability to quickly clean up these conntrack entries, or reuse ephemeral ports, gets confused. It can result in stale, lingering nf_conntrack entries that think a connection is still active or in TIME_WAIT, even when the application layer considers it long gone.
When your Node.js app tries to initiate a new connection using an ephemeral source port that the kernel's conntrack table still incorrectly flags as "in use" (because of the timing issue under load), the kernel silently drops the packet or refuses to establish the connection, leading directly to an ETIMEDOUT at the application level. It's a race condition exacerbated by CPU starvation. It's related to the DNS resolution issues we've seen before, which I've discussed in The EAI_AGAIN Spectre: Node.js, Docker, and CentOS 7's Stale nf_conntrack Dance.
The Sledgehammer Solution
Since upgrading your kernel or OS isn't always an option in an enterprise environment, we're going for a direct, albeit somewhat blunt, fix. You need to significantly increase the maximum size of the nf_conntrack table and reduce its idle timeout for TCP connections that are in a problematic state. This gives the kernel more room to manage potentially stale entries and cleans them up faster.
Execute this on your Docker host machine:
sudo sysctl -w net.netfilter.nf_conntrack_max=1048576
sudo sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30
sudo sysctl -w net.netfilter.nf_conntrack_tcp_timeout_fin_wait=30
sudo sysctl -w net.netfilter.nf_conntrack_tcp_timeout_close=10
sudo sysctl -w net.nf_conntrack_max=1048576To make these changes persistent across reboots, add them to /etc/sysctl.conf or a new file like /etc/sysctl.d/99-conntrack.conf:
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 30
net.netfilter.nf_conntrack_tcp_timeout_close = 10
net.nf_conntrack_max = 1048576Then run sudo sysctl -p to apply.
Why This Works (and Why It's Ugly)
Increasing nf_conntrack_max from its default (often 65536 or 131072) to 1,048,576 gives the kernel a much larger capacity to hold connection tracking entries. This buys you headroom against the intermittent failure to prune stale entries under CPU load. By dramatically reducing nf_conntrack_tcp_timeout_time_wait, fin_wait, and close, you're telling the kernel to purge these entries much more aggressively. This reduces the window during which a stale entry can block a new connection attempt.
It's ugly because you're masking a kernel bug with brute force. You're consuming more memory for the conntrack table and potentially prematurely closing some connections if your network stack isn't perfectly aligned, though for typical web services, this is a relatively safe trade-off. However, it's a workaround. The true fix is a kernel upgrade.
Prevention and Monitoring
You can monitor your nf_conntrack usage with cat /proc/sys/net/netfilter/nf_conntrack_count and compare it against nf_conntrack_max. If you see it consistently nearing the max, you've got problems. Keep an eye on your ETIMEDOUT rates in your application metrics. A sudden spike during CPU-intensive periods is your tell-tale sign. For more complex network debugging in Docker Swarm environments, you might also find insights in EAI_AGAIN Hell: The Ghost iptables Rule Haunting Node.js in Docker Swarm, as conntrack issues often intertwine with iptables.
This fix has saved our bacon multiple times in legacy environments where OS upgrades weren't feasible. It's not pretty, but it gets the job done. Now go, fix your damn application, and get some sleep.
Comments
Post a Comment