Introduction

fetch in modern Node.js uses connection pooling under the hood. That improves throughput, but it also means the client may reuse a socket that an upstream proxy or load balancer already closed because it sat idle too long. The next request lands on a dead connection and surfaces as socket hang up, ECONNRESET, or a generic fetch failure.

Symptoms

  • Requests fail intermittently instead of on every call
  • The same endpoint succeeds when retries happen immediately afterward
  • Errors become more common during low traffic or bursty traffic patterns
  • Proxy, ALB, or reverse proxy logs show connection resets around the same time

Common Causes

  • The client keeps sockets alive longer than the upstream proxy allows
  • A reverse proxy closes idle upstream connections aggressively
  • The app reuses one global dispatcher with default timings that do not match the network path
  • Idempotent requests are not retried when a stale socket is detected

Step-by-Step Fix

  1. 1.Confirm the error happens on a reused idle socket
  2. 2.Turn on transport debugging and compare the error timing with upstream idle timeout settings.
bash
NODE_DEBUG=http,net node server.js
  1. 1.Lower the client keep-alive window below the proxy idle timeout
  2. 2.Make the Node client retire idle sockets before the proxy does.

```javascript import { Agent, setGlobalDispatcher } from "undici";

setGlobalDispatcher( new Agent({ keepAliveTimeout: 4000, keepAliveMaxTimeout: 4000, connections: 50, }), ); ```

  1. 1.Review the proxy or load balancer timeout on the same request path
  2. 2.The client and proxy need compatible timeout assumptions or pooled sockets become unreliable.

```nginx upstream api_backend { server 127.0.0.1:8080; keepalive 64; }

server { location / { proxy_http_version 1.1; proxy_set_header Connection ""; proxy_read_timeout 30s; } } ```

  1. 1.Retry only safe requests when the reused socket was stale
  2. 2.A single retry is usually enough for idempotent reads, but you should not hide the problem with unlimited retries.
javascript
for (let attempt = 0; attempt < 2; attempt += 1) {
  try {
    return await fetch(url, { method: "GET" });
  } catch (error) {
    if (attempt === 1 || error.cause?.code !== "ECONNRESET") {
      throw error;
    }
  }
}

Prevention

  • Set explicit keep-alive timeouts instead of relying on library defaults
  • Document idle timeout values for every proxy and load balancer in the request path
  • Treat ECONNRESET spikes as a transport issue first, not an application bug first
  • Restrict automatic retries to idempotent operations