Article View

Scroll down to read the full article.

The Ghost in the Machine: Node.js ECONNRESET After Exactly 60 Seconds of Silence (Linux Conntrack Nightmare)

calendar_month August 13, 2026 |
Quick Summary: Battling Node.js ECONNRESET errors on Linux after 60s idle? This guide dissects the conntrack tcp_timeout_close trap, offering an SRE's fix.

Alright, listen up. If you're here, you've probably spent the last few days pulling your hair out, staring at inscrutable Node.js logs spitting out ECONNRESET or 'write EPIPE' errors. And here's the kicker: it only happens after your application has been blissfully idle for exactly 60 seconds, then tries to make an outbound HTTP call. It's a ghost in the machine, a silent killer, and it's almost certainly Linux conntrack silently murdering your TCP connections.

This isn't some esoteric Node.js bug. This is fundamental networking infrastructure biting you because of a default setting that assumes too much. We've seen this particular brand of hell in setups using transparent proxies, iptables-based load balancing, or any scenario where connection tracking is actively managing your egress traffic. It's subtle, it's frustrating, and it costs you valuable production uptime.

The Symptoms: A 60-Second Executioner

  • Your Node.js service attempts an outbound HTTP/HTTPS request to an external API.
  • If the connection is fresh, it works fine.
  • If the connection has been idle for exactly 60 seconds (or sometimes 180 seconds, but 60 is more common for this specific issue) and then reused from the Node.js HTTP agent's pool, BAM! ECONNRESET.
  • Stack traces point to network write operations, like socket.write.
  • The problem vanishes under heavy load, only to reappear when traffic ebbs. Because, of course.
  • Often co-occurs with deployments of new Linux kernels or network appliance updates.

A grim reaper figure in a dark server room
Visual representation

The Environments Where This Error Triggers

This isn't universal. Specific combinations of kernel and Node.js versions, interacting with netfilter defaults, are the culprits.

Operating System / Kernel Version Node.js Version Range Observed Impact
Linux Kernel 4.x (e.g., 4.4, 4.15, 4.19) 12.x, 14.x, 16.x High likelihood of ECONNRESET on idle connection reuse.
Linux Kernel 5.x (e.g., 5.4, 5.10) 14.x, 16.x, 18.x Moderate likelihood, sometimes longer timeouts (e.g., 180s).
Any system with explicit netfilter conntrack TCP timeouts configured All supported Node.js versions High likelihood if tcp_timeout_close is 60s/180s.

The Root Cause

Here’s the deal: Node.js, by default, uses an HTTP agent that keeps connections alive for reuse (keepAlive is true). It has its own idle timeout (keepAliveMsecs, typically 5000ms by default, but the underlying OS connection might persist longer). When Node.js thinks a connection is still good, it reuses it. But what if the operating system's network stack has silently decided that connection is dead?

Enter netfilter conntrack. This is Linux's connection tracking subsystem, fundamental for NAT, firewalls, and transparent proxies. It tracks the state of every TCP connection passing through it. Critically, it has various timeouts for different TCP states. The one that bites us here is often tcp_timeout_close or tcp_timeout_close_wait. Their default value on many kernels is 60 seconds (or sometimes 180s). This means that after a TCP connection transitions to a FIN_WAIT, LAST_ACK, or CLOSE state, conntrack will purge its entry after 60 seconds of inactivity.

The architectural flaw? Node.js reuses a connection from its pool. The connection looks fine from Node.js's perspective. But the network device (or the kernel's conntrack module itself, if it's acting as a transparent proxy) has silently torn down the state for that particular TCP session after 60 seconds of no data. The moment Node.js tries to write the first byte to this 'ghost' connection, the kernel responds with an unexpected RST packet (or simply refuses to send data), and Node.js throws ECONNRESET or EPIPE. It's a race condition between the application-layer keep-alive and the kernel's stateful packet inspection.

A detailed schematic of TCP connection states with a timer icon ominously counting down from 60 seconds
Visual representation

The Fix: Forcing conntrack to Play Nice

We need to synchronize conntrack's understanding of a 'closed' connection with what Node.js (or any application with aggressive keep-alive) expects. The simplest, most effective fix is to reduce the conntrack timeout for closed TCP connections.

Option 1: Adjust conntrack timeouts (Recommended)

This is the surgical strike. We tell conntrack to drop its state for closed TCP connections much faster. A value like 10 seconds is usually sufficient, giving application-level keep-alives a better chance to manage their own lifecycle.


# Check current timeouts (values are in seconds)
cat /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established
cat /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_close
cat /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_close_wait
cat /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_fin_wait

# Adjust the problematic timeout. We typically target 'close' and 'close_wait' first.
# Set this to a value lower than your application's expected idle time, but not too low (e.g., 5-10s).
# For Node.js with default keepAliveMsecs=5000 (5s), 10 seconds for conntrack is a safe bet.

echo 10 > /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_close
echo 10 > /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_close_wait
echo 10 > /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_fin_wait

# To make this persistent across reboots, add to /etc/sysctl.conf or a new file in /etc/sysctl.d/
# Example for /etc/sysctl.d/99-conntrack-fix.conf:
# net.netfilter.nf_conntrack_tcp_timeout_close = 10
# net.netfilter.nf_conntrack_tcp_timeout_close_wait = 10
# net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 10
# Then run: sysctl -p

Why this works: By reducing conntrack's timeouts, you're telling the kernel, "If a connection is closing, don't hold onto its state for ages." This forces the kernel to drop its tracking state quickly, aligning it more closely with the application's connection lifecycle. This ensures that if Node.js (or any app) tries to reuse a connection, conntrack hasn't already forgotten about it.

Option 2: Disable Node.js keepAlive for problematic services (Less Ideal)

If you absolutely cannot touch kernel parameters (though you really should push for it), you can explicitly disable keepAlive for the HTTP agent used to connect to the problematic external service. This means new TCP connections for every request, which incurs more overhead, but guarantees you won't hit a ghost connection.


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

const agent = new http.Agent({ keepAlive: false }); // For HTTP
const httpsAgent = new https.Agent({ keepAlive: false }); // For HTTPS

// Usage example:
// http.get('http://example.com/api', { agent: agent }, (res) => { /* ... */ });
// https.get('https://api.external.com', { agent: httpsAgent }, (res) => { /* ... */ });

This is a workaround, not a solution. It trades a mysterious error for predictable (but higher) network overhead. Think twice before opting for this if you care about latency and resource usage. For more insights on optimizing network interactions, you might want to review strategies discussed in Execution Path Zero: The Relentless Pursuit of Nanosecond Supremacy in Algorithmic Trading.

Option 3: Increase Node.js keepAliveMsecs (Risky)

You could try increasing Node.js's keepAliveMsecs to be *greater* than conntrack's default 60 seconds (e.g., 70 seconds). This attempts to make Node.js proactively close connections before conntrack does. However, this holds onto sockets longer than necessary and can lead to its own resource exhaustion issues under load. Plus, it only works if conntrack's timeout is consistently 60s, and not, say, 180s on a different kernel. It’s a game of chicken you likely won't win sustainably. If your system is already struggling with managing connections, consider architectural changes as explored in Architecting for Chaos: Scaling Distributed Systems in the FAANG Crucible.

Final Thoughts

This issue is a prime example of how seemingly unrelated layers of your stack – application runtime, kernel networking, and network infrastructure – can conspire to create obscure, intermittent failures. Always start your debugging by questioning defaults. Especially when you see errors after precise time intervals, you're looking at a timeout, and those are often in the network stack or OS configuration. Don't let the ghost of conntrack haunt your Node.js apps. Fix it at the source.

Discussion

Comments

Read Next