Article View

Scroll down to read the full article.

The Phantom Reset: Node.js keepAlive and Kernel Scheduling Under Load

calendar_month August 27, 2026 |
Quick Summary: Troubleshoot frustrating ECONNRESET errors in Node.js applications using http.Agent keepAlive on specific Linux kernels under high CPU load. Get a...

Alright, listen up. If you've ever dealt with a Node.js application that sporadically throws ECONNRESET or socket hang up errors on outbound HTTP requests, but only when that Node process is absolutely thrashing its CPU, then you're in for a treat. This isn't your typical network issue. This is a subtle, insidious dance between Node.js's event loop, its http.Agent keepAlive mechanism, and specific quirks of older Linux kernel network stacks. I've wasted too many weekends on this, so you don't have to.

The Symptom: Sporadic Outbound Request Failures

Your application logs are filled with ECONNRESET, socket hang up, or even EPIPE errors, specifically when making outbound API calls (e.g., to a database, another microservice, or a third-party API). What's maddening is that the remote service is fine. Network latency is fine. Other services on the same box are fine. But your CPU-bound Node.js service just keeps dropping connections. It doesn't happen when CPU usage is low. It only happens when the single-threaded Node.js event loop is completely pegged, often after processing a large payload or doing intense synchronous computation.

Environments Affected

This particular beast thrives in specific conditions. If your setup matches, pay close attention.

Operating System Kernel Version Node.js Version HTTP Client Condition
Ubuntu LTS 4.15.x - 4.19.x 12.x, 14.x, 16.x http.Agent (native), axios, node-fetch keepAlive: true, CPU > 90%
CentOS 7 / RHEL 7 3.10.x (patched to 4.x equivalent) 12.x, 14.x http.Agent (native), request keepAlive: true, CPU > 90%
AWS EC2 (older AMIs) 4.9.x - 4.14.x 12.x, 14.x, 16.x Any using native http.Agent keepAlive: true, CPU > 90%

If you're on a newer kernel (5.x+) or a modern Node.js version (18.x+), you're likely safe. But for those stuck on legacy systems, this is your nightmare.

Tangled
Visual representation

Initial Head-Scratching

Naturally, you've already checked the usual suspects:

  • Remote Service Health: Up, running, no errors.
  • Network ACLs/Firewalls: Not blocking.
  • Resource Limits: No ulimit -n issues, plenty of memory.
  • Load Balancer Timeouts: Configured correctly, not prematurely killing connections.
  • Node.js Memory Leaks: Heap looks stable, GC cycles are fine.

You might even have tried adjusting http.globalAgent.maxSockets or similar, but the problem persists, seemingly at random.

Digging Deeper: TCP Dumps and Strace

This is where it gets interesting. A tcpdump on the Node.js host during an incident often reveals the ECONNRESET comes from the local machine itself ([R.] flag from localhost IP). This isn't the remote server saying "go away." This is your own kernel telling your application that the socket it's trying to write to is suddenly invalid. A strace of the Node.js process might show a write() call returning EPIPE or ECONNRESET.

This kind of deep dive into system calls and network behavior is crucial, much like understanding the nitty-gritty of architecting battle-hardened, complex workflows in n8n where low-level details often determine reliability.

The Root Cause

The core problem stems from a race condition and state mismatch between the CPU-starved Node.js application and the Linux kernel's TCP stack. Here's the breakdown:

  1. Node.js keepAlive Behavior: Your Node.js application, using http.Agent with keepAlive: true, diligently reuses established TCP connections for subsequent requests to the same host. This reduces overhead and latency.
  2. CPU Starvation: When your Node.js process becomes CPU-bound (e.g., due to heavy synchronous work, large JSON parsing, regex hell, etc.), its single event loop struggles to process events promptly. This includes network events.
  3. Remote Server Closes Connection: The remote server, after responding to a request, might send a FIN packet to gracefully close its end of the TCP connection. This is normal.
  4. Kernel's View vs. Node.js's View: Because Node.js is CPU-starved, it doesn't get scheduled to read the incoming FIN packet from its socket receive buffer promptly. The kernel, however, has received the FIN and transitions its internal state for that socket (e.g., to FIN_WAIT2).
  5. Kernel Timeout & Cleanup: On specific older Linux kernel versions, if the application doesn't read the FIN and acknowledge it (send an ACK) within a certain period (governed by parameters like net.ipv4.tcp_fin_timeout), the kernel can become impatient. It might prematurely transition the socket state to CLOSED or even drop the internal file descriptor reference, perceiving the connection as stale or abandoned by the local application.
  6. The Reset: When Node.js finally gets scheduled and attempts to reuse this connection from its keepAlive pool, it tries to write to what it thinks is an ESTABLISHED socket. But from the kernel's perspective, that file descriptor either no longer exists as an active connection or is in a state where writing is invalid. The kernel then generates an immediate RST (reset) packet from the local machine, signaling the application that the connection is dead, resulting in the dreaded ECONNRESET.

This is a subtle race condition amplified by process scheduling latency under extreme load. Newer kernels and Node.js versions have better handling of these scenarios, but older stacks are highly susceptible.

A rusty
Visual representation

The 'Fix' (Until You Upgrade)

The ultimate solution is to upgrade your Linux kernel and Node.js runtime. Seriously, don't ignore that. But when that's not an option right now, here are two critical changes you can make to mitigate this:

1. Kernel TCP Timeout Adjustment

We need to make the kernel more patient. Increase the tcp_fin_timeout to give your CPU-starved Node.js process more breathing room to acknowledge incoming FIN packets. The default is often 60 seconds; we'll bump it.


# Check current value (usually 60)
sysctl net.ipv4.tcp_fin_timeout

# Increase to 120 seconds (2 minutes)
sudo sysctl -w net.ipv4.tcp_fin_timeout=120

# Make permanent across reboots
sudo echo "net.ipv4.tcp_fin_timeout=120" >> /etc/sysctl.conf
sudo sysctl -p

Be cautious when tweaking kernel parameters, and monitor your system. This change impacts how quickly connections in FIN_WAIT2 state are cleaned up globally.

2. Node.js http.Agent keepAlive Timeout

Secondly, tell Node.js to be less aggressive about keeping connections alive when it's very busy. By setting a very short keepAliveMsecs on the http.Agent, you force Node.js to close and re-establish connections more frequently. This is less performant but prevents it from trying to reuse a socket that the kernel has already secretly given up on. A very low value (e.g., 1000ms or 500ms) works as a stopgap.


const http = require('http');
const https = require('https');

const agentOptions = {
  keepAlive: true,
  maxSockets: 100, // Adjust as needed
  keepAliveMsecs: 1000, // IMPORTANT: Shorter than default, forces quicker cleanup
  freeSocketTimeout: 500 // Also important: How long a socket stays idle in the pool
};

const httpAgent = new http.Agent(agentOptions);
const httpsAgent = new https.Agent(agentOptions);

// Apply to your http/https requests
// Example with native fetch (Node.js 18+):
// fetch('http://example.com', { agent: httpAgent });

// Example with axios:
// axios.create({
//   httpAgent: httpAgent,
//   httpsAgent: httpsAgent
// });

// Example with native http.request:
// http.request({
//   hostname: 'example.com',
//   agent: httpAgent,
//   // ... other options
// });

You need to ensure that all your outbound HTTP/HTTPS requests are using an agent configured with these parameters. If you're using libraries that don't expose agent configuration easily, you might have to monkey-patch http.globalAgent and https.globalAgent, though that's generally discouraged. This careful management of connection pools is as vital as picking the right framework, like debating Next.js vs. Nuxt.js for enterprise web development – the details matter.

Why This Matters

This isn't just about a pesky error message. Sporadic connection resets under load can lead to cascading failures, service degradation, and extremely difficult-to-debug production incidents. It's a reminder that even in a highly abstracted world, the low-level interactions between your application runtime and the operating system's kernel are paramount. While these workarounds can buy you time, they don't solve the underlying problem. Prioritize upgrading your infrastructure.

Discussion

Comments

Read Next