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.Confirm the error happens on a reused idle socket
- 2.Turn on transport debugging and compare the error timing with upstream idle timeout settings.
NODE_DEBUG=http,net node server.js- 1.Lower the client keep-alive window below the proxy idle timeout
- 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.Review the proxy or load balancer timeout on the same request path
- 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.Retry only safe requests when the reused socket was stale
- 2.A single retry is usually enough for idempotent reads, but you should not hide the problem with unlimited retries.
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
ECONNRESETspikes as a transport issue first, not an application bug first - Restrict automatic retries to idempotent operations