Article View

Scroll down to read the full article.

The Silent Socket Killer: Node.js, ALB Idle Timeouts, and Containerized ECONNRESETs

calendar_month July 14, 2026 |
Quick Summary: Debugging `ECONNRESET` in Node.js apps on Kubernetes. Learn how AWS ALB idle timeouts and default `http.Agent` settings cause silent connection te...

Alright, listen up. If you've ever dealt with Node.js microservices humming along in Kubernetes, making rapid-fire HTTP calls to internal services behind an AWS Application Load Balancer (ALB), and then out of nowhere, you start seeing intermittent ECONNRESET or socket hang up errors, this one's for you. This isn't your garden-variety ephemeral port exhaustion, nor is it the infamous TCP_TW_RECYCLE trap. This is far more insidious, a silent killer that will make you question your sanity, your code, and possibly your life choices.

I’ve seen this exact scenario play out on countless teams, leading to frantic late-night debugging sessions. The symptoms are always the same: your Node.js pod seems fine, no crazy CPU spikes, plenty of memory. netstat on the client side shows available ephemeral ports. Yet, your application logs are peppered with these frustrating connection resets, often after several hours of smooth operation. It feels random, but it's not. It's a fundamental mismatch between Node.js's default connection pooling and how modern cloud load balancers manage idle connections.

The Symptoms, Unpacked

  • Intermittent ECONNRESET or socket hang up errors: These are the primary culprits in your logs. They're not always consistent, making them a nightmare to reproduce.
  • Happens after a period of uptime: The errors rarely appear right after a deployment. They tend to manifest hours later, once the connection pool has cycled through enough stale connections.
  • Specific to outbound HTTP/HTTPS requests: Your service fails when attempting to talk to *another* internal service, typically via a hostname resolving to an internal load balancer (like an ALB or NLB).
  • netstat on client (Node.js pod) looks normal: You check the client side, and there are no overwhelming numbers of TIME_WAIT or CLOSE_WAIT states, and plenty of ephemeral ports are free. This is the deceptive part.
  • But netstat on the *target* service (or its proxy) might tell a different story: If you could inspect the server-side of the connection (the actual backend instance behind the ALB), you might see its logs or network metrics indicating connection closures or resets, but from the perspective of the load balancer.

It's infuriating because all your basic network debugging points to 'nothing's wrong,' yet your application is screaming.

Environments Where This Error Thrives

This problem is particularly prevalent in containerized environments where services make frequent, short-lived HTTP requests to other services:

Operating System (Container Base Image) Node.js Version Container Orchestration Load Balancer/Proxy
CentOS 7/8, RHEL UBI 8/9, Ubuntu 20.04+ 12.x, 14.x, 16.x, 18.x (using default http.Agent) Kubernetes, Docker Swarm, ECS Fargate AWS ALB, AWS NLB, Nginx (with default keepalive_timeout), Istio Sidecars

An intricate network diagram showing a dead connection being reused with an 'X' over a wire segment
Visual representation

The Root Cause

Here's the architectural flaw that bites us:

AWS ALBs (and many other proxies like Nginx, or even cloud-managed service meshes like Istio) have an idle timeout. For an ALB, this defaults to 60 seconds. What this means is, if a connection between the ALB and its target (your backend service) remains idle for 60 seconds, the ALB will silently close that connection from its side. It doesn't send a FIN packet to your backend. It just drops it. This is crucial.

Node.js, by default, uses an http.Agent (or https.Agent) that implements connection pooling with keepAlive: true. This is great for performance, as it reuses existing TCP connections for subsequent requests to the same host, avoiding the overhead of new TCP handshakes and TLS negotiations. The problem? Node.js's agent has no idea that the ALB has silently terminated the connection.

So, what happens? Your Node.js application makes a request, gets a connection from the pool, sends data, and gets a response. The connection sits idle for 65 seconds. The ALB, after 60 seconds of idle time, kills its side of the connection to your backend. The Node.js client still thinks the connection is perfectly valid in its pool. The next time your application needs to talk to that service, it grabs this now-stale connection from the pool and tries to send data. The OS/network firewall (or even the target service itself) sees an unexpected packet on a connection that it believes is closed (or was never fully established from its perspective, as the ALB dropped it), and immediately responds with an RST packet. This translates directly to an ECONNRESET on the Node.js client.

This is different from general ephemeral port exhaustion because the client isn't running out of ports; it's trying to use a port on a connection that the server-side has silently abandoned. It's also distinct from the TCP_TW_RECYCLE trap because the client's `TIME_WAIT` states aren't the primary issue; rather, it's the server's (ALB's) aggressive idle connection management.

The Solution: Override the Agent with Explicit Keep-Alive Settings

The fix is deceptively simple: tell Node.js's http.Agent to close its pooled connections *before* the load balancer has a chance to silently murder them. You need to explicitly configure your own http.Agent (or https.Agent) with a keepAliveMsecs value that is safely below the load balancer's idle timeout.

For an AWS ALB with a default 60-second idle timeout, aim for something like 50 seconds (50,000 milliseconds). This gives your Node.js application a 10-second buffer to proactively close idle connections before the ALB pulls the rug out from under it. When a connection is proactively closed by the agent, it's removed from the pool, and a new connection will be established for the next request, avoiding the reset.

A complex
Visual representation

Implementation: Copy-Paste This Code

You need to create a custom agent and pass it to your HTTP client (e.g., axios, node-fetch, or the native http/https module). Make sure this agent is a singleton, reused across all requests to the same service.


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

// --- Configuration --- 
// Set this to be safely below your Load Balancer's idle timeout. 
// AWS ALB default is 60 seconds (60000ms). Aim for 50 seconds.
const ALB_IDLE_TIMEOUT_MS = 50 * 1000; 

// --- Custom Agents --- 
const httpAgent = new http.Agent({
    keepAlive: true,        // Essential for connection pooling
    keepAliveMsecs: ALB_IDLE_TIMEOUT_MS, // How long an idle socket should remain open (crucial!)
    maxSockets: 100,        // Max sockets per host (adjust as needed)
    maxFreeSockets: 10,     // Max free sockets to keep in pool (Node.js 15+ has freeSocketTimeout)
});

const httpsAgent = new https.Agent({
    keepAlive: true,
    keepAliveMsecs: ALB_IDLE_TIMEOUT_MS,
    maxSockets: 100,
    maxFreeSockets: 10,
});

// --- How to use with native http/https module --- 
function makeHttpRequest(url, options = {}) {
    const agent = url.startsWith('https') ? httpsAgent : httpAgent;
    return new Promise((resolve, reject) => {
        const req = (url.startsWith('https') ? https : http).request({
            ...options,
            agent: agent, // Use our custom agent
            host: new URL(url).hostname,
            port: new URL(url).port || (url.startsWith('https') ? 443 : 80),
            path: new URL(url).pathname + new URL(url).search,
        }, (res) => {
            // Handle response
            res.on('data', (d) => process.stdout.write(d));
            res.on('end', () => resolve(res.statusCode));
        });

        req.on('error', (e) => {
            console.error(`Problem with request: ${e.message}`);
            reject(e);
        });
        req.end();
    });
}

// --- How to use with axios (example) --- 
const axios = require('axios');

const axiosHttp = axios.create({
    httpAgent: httpAgent,
    httpsAgent: httpsAgent,
    // Other axios configurations
});

// Now use axiosHttp for your requests
// axiosHttp.get('http://internal-service.example.com/api/data');
// axiosHttp.post('https://secure-internal-service.example.com/api/save', payload);

Why This Is So Tricky

This problem is a quintessential SRE headache because it spans multiple layers:

  • Application Layer: Node.js's default behavior.
  • Network Layer: How load balancers manage TCP connections.
  • Operational Layer: Containerization and microservice architectures.

It's a silent interaction. The ALB doesn't tell your Node.js app it's closing connections. Your Node.js app doesn't know it's trying to use a dead connection until it hits an error. It requires a deep understanding of how each component functions at a low level to diagnose properly. Many times, it's mistaken for general network instability or even bugs in the application code, leading teams down endless rabbit holes.

Final Advice

If your Node.js application is a microservice making frequent outbound calls, especially in a cloud environment:

  • Always be explicit: Don't rely on Node.js's default http.Agent behavior. Define your own.
  • Know your infrastructure: Understand the idle timeouts of all proxies and load balancers between your service and its dependencies.
  • Monitor: Keep an eye on network metrics, especially connection states on both client and server sides, to spot anomalies early.

Being proactive with your agent configuration is a small change that can save you countless hours of troubleshooting. Trust me, your future self will thank you.

Read Next