pickuma.
Infrastructure

Debugging Occasional ECONNRESET Errors in Node.js: Root Causes and Fixes

ECONNRESET in Node.js usually traces to an idle connection closed by a load balancer or proxy while your keep-alive pool still holds it. Here is how to find the real cause and fix it.

6 min read

It passes every test on your laptop. It passes in staging. Then production logs a handful of Error: read ECONNRESET lines a day — never the same endpoint twice, never reproducible on demand. You add a retry, the count drops, and you move on without knowing what happened.

That is the worst outcome, because the error is usually telling you something specific about how your connections are managed. Here is what ECONNRESET means, why it clusters around idle connections, and the changes that actually stop it.

What ECONNRESET actually means

ECONNRESET means the peer sent a TCP RST packet. The connection was open and working, and then the other side discarded its half and told your kernel to stop using it. Node surfaces this as an error with code: 'ECONNRESET' and, often, syscall: 'read' — you were waiting to read a response and the socket died underneath you.

It is not the same as the two errors people confuse it with:

ErrorWhat happened
ECONNREFUSEDNothing accepted the connection — wrong port, or the service is down
ETIMEDOUTThe connection or response never completed within the allowed time
ECONNRESETThe connection was established, then the peer abruptly killed it

The word that matters is abruptly. Something on the other end was fine a moment ago and then was not. That narrows the search: you are not looking for a server that is down, you are looking for a connection that got closed while you still thought you owned it.

Why it is almost always an idle connection

Modern Node HTTP clients reuse connections. Node’s http.Agent with keepAlive: true — the default behavior behind globalThis.fetch and undici in Node 18+ — keeps TCP sockets in a pool after each response so the next request skips the TCP and TLS handshake. That is good for latency. It is also the most common source of intermittent resets.

A pooled socket can be closed by the other side while it sits idle in your pool. You will not find out until you write the next request onto it. Your client believes the socket is alive; the server or load balancer already sent a FIN or RST; you push bytes into a dead connection and the reset comes back as ECONNRESET.

Almost everything between you and the origin closes idle connections on a timer:

  • AWS Application Load Balancers default to a 60-second idle timeout.
  • nginx defaults keepalive_timeout to 75 seconds.
  • A Node origin server’s server.keepAliveTimeout defaults to 5000 ms.

That gives you a rule that prevents the race: the side that sends requests should give up an idle socket before the side that receives them. When your client holds idle sockets longer than the load balancer does, the load balancer wins the race every time and you eat resets.

The same root cause bites Node servers from the other direction. Node’s 5-second keepAliveTimeout is shorter than an ALB’s 60-second idle timeout, so the ALB keeps a socket it believes is reusable, sends a request into a connection Node already closed, and returns a 502 to your user. Same race, opposite roles.

Two more causes worth ruling in or out: a burst of ECONNRESET during a rolling deploy is expected, because in-flight connections to terminating instances get reset — if the spike lines up with a deploy timestamp, that is the explanation. And an upstream that was OOM-killed or crashed will reset every connection it held.

Fixes that hold up in production

Align your timeouts. The ordering you want is: client idle timeout is shorter than the load balancer idle timeout, which is shorter than the origin server idle timeout. For a Node server sitting behind an ALB, raise its timeouts above the load balancer’s:

const server = http.createServer(app);
server.keepAliveTimeout = 65_000; // longer than the ALB 60s idle timeout
server.headersTimeout = 66_000; // must exceed keepAliveTimeout

For a client behind that same ALB, do the opposite — keep idle sockets for less than 60 seconds so you retire them before the load balancer can.

Retry idempotent requests once. A reset on an idle pooled socket almost always means the request never reached the application — it died on the wire before the server read it. For GET, HEAD, PUT, and DELETE, a single retry on a fresh connection is safe and clears the large majority of these errors. Be deliberate with POST: only retry when you can confirm the request never landed, or you risk a duplicate write.

Prefer undici’s pool. Node’s built-in fetch is backed by undici, whose connection pool tracks the server’s Keep-Alive response header and recycles sockets more carefully than the legacy http.Agent. If you are still on a hand-rolled agent, moving to undici removes a class of stale-socket bugs.

Tracing one reset through an agent pool, a retry wrapper, and three layers of timeout config means reading across files that rarely sit next to each other. An editor that can follow that path quickly is worth having open.

Cursor

An AI-native code editor that reads across files to trace retry logic, connection-pool configuration, and timeout settings — useful when a bug spans your HTTP client, your server setup, and your infra config.

Free tier; Pro at $20/month

Try Cursor

Affiliate link · We earn a commission at no cost to you.

FAQ

Is ECONNRESET always a bug in my code? +
No. It often means a healthy peer closed an idle connection exactly on schedule. It becomes your bug when your client reuses that socket without expecting the close, or when your keep-alive timeouts are ordered so the wrong side closes first.
Will enabling TCP keepalive fix ECONNRESET? +
Rarely. TCP keepalive probes idle connections, but operating systems wait around two hours before the first probe by default, so it cannot catch a load balancer that closes sockets after 60 seconds. Aligning HTTP keep-alive timeouts is the fix that works.
Should I retry automatically on ECONNRESET? +
Yes for idempotent requests — GET, HEAD, PUT, DELETE — with one retry on a fresh socket and a short backoff. For POST, retry only when you can confirm the request never reached the server, otherwise you risk processing it twice.

Related tools

Some links above are affiliate links. We may earn a commission if you sign up. See our disclosure for details.

Related reading

See all Infrastructure articles →

Get the best tools, weekly

One email every Friday. No spam, unsubscribe anytime.