Error: socket hang up in Node.js (tested)

Error: socket hang up with code: 'ECONNRESET' means the connection closed before a response arrived. The cause can be the server, the client, or a stale keep-alive socket. We reproduced each cause in Node 24 and recorded what http.request, fetch and axios report.

1. What "socket hang up" means

Node's http client throws Error: socket hang up when the socket closes after the request was sent but before any response headers came back. The error code is always ECONNRESET, even when the close was a clean FIN and not a TCP reset.

If the headers already arrived and the body is cut off, you get a different error: Error: aborted on the response object. Built-in fetch (undici) never says "socket hang up". It throws TypeError: fetch failed and puts the real reason in err.cause.

2. Our test setup

We ran a Node 24.21.0 HTTP server and an HTTPS server on localhost, plus a raw TCP server that speaks minimal HTTP/1.1. Each scenario was hit with three clients: http.request, built-in fetch, and axios 1.20.0. We logged err.name, err.message, err.code and err.cause.

3. Results: the exact error per cause and client

Causehttp.requestfetch (undici)axios 1.20.0
Server destroys socket without responding (req.socket.destroy())Error: socket hang up, code ECONNRESETTypeError: fetch failed, cause SocketError: other side closed, code UND_ERR_SOCKETError: socket hang up, code ECONNRESET
Server process crashes mid-request (uncaught throw in handler)socket hang up / ECONNRESETfetch failed / other side closed / UND_ERR_SOCKETsocket hang up / ECONNRESET
Next request after the crash (nothing listening)Error: connect ECONNREFUSED 127.0.0.1:8993 (http.request)
Server sends headers, then destroys socket mid-bodyError: aborted, code ECONNRESET (on the response)TypeError: terminated, cause other side closedAxiosError: stream has been aborted, code ERR_BAD_RESPONSE
Server idle timeout (server.setTimeout(500), handler takes 3 s)socket hang up / ECONNRESET at 503 msfetch failed / other side closed at 503 mssocket hang up / ECONNRESET
Plain http:// to an HTTPS portsocket hang up / ECONNRESET (3 ms)fetch failed / other side closedsocket hang up / ECONNRESET
https:// to a plain HTTP portError: write EPROTO ... wrong version number, code EPROTOfetch failed, cause code ERR_SSL_WRONG_VERSION_NUMBERwrite EPROTO ... wrong version number, code EPROTO
Stale keep-alive socket (see section 5)socket hang up or read ECONNRESET, req.reusedSocket = truefetch failed, cause read ECONNRESETsocket hang up or read ECONNRESET

curl against the "destroy without responding" server prints curl: (52) Empty reply from server. That is the same event seen from curl.

4. When the client hangs up itself

A "socket hang up" can be your own code. Calling req.destroy() before the response arrives gives exactly the same message as a server-side close. This is the most confusing case.

Client action (server takes 3 s)Error
req.destroy() after 300 msError: socket hang up, code ECONNRESET at 304 ms
timeout: 500 + req.on('timeout', () => req.destroy())Error: socket hang up, code ECONNRESET at 504 ms
timeout: 500 with no 'timeout' handlerNo error. The request waited and got 200 after 3 s.
fetch + AbortSignal.timeout(300)TimeoutError: The operation was aborted due to timeout
fetch + AbortController.abort()AbortError: This operation was aborted
axios timeout: 300AxiosError: timeout of 300ms exceeded, code ECONNABORTED
axios signal abortedCanceledError: canceled, code ERR_CANCELED

Two lessons. The timeout option of http.request only emits an event; it does not cancel anything. And if you destroy on timeout, you get "socket hang up", which looks like a server fault. Pass an error to make it clear: req.destroy(new Error('client timeout')). The server sees this as a client abort, which nginx logs as 499 Client Closed Request.

5. The stale keep-alive race

In Node 24 http.globalAgent has keepAlive: true (we printed its options). Idle sockets are reused. If the server closes an idle socket at the same moment the client sends a new request on it, the request fails.

We built a raw TCP server that closes idle connections after 1000 ms and does not send a Keep-Alive: timeout header (like many proxies and non-Node servers). We sent a request, waited 970 to 1030 ms, then sent another.

ClientFailuresWhenError
http.request, global agent2 of 31998 and 1000 ms idleread ECONNRESET and socket hang up, both with reusedSocket true
axios default2 of 31998 and 1000 ms idleread ECONNRESET and socket hang up
fetch0 of 31 in the sweep; 3 of 24 in a denser 994–1001 ms run997 ms idlefetch failed, cause read ECONNRESET
http.request, new http.Agent({keepAlive: true, timeout: 900})0 of 24994–1001 msNone. Every request used a new socket.

The window is a few milliseconds wide. In production, with thousands of requests, it turns into a steady trickle of random errors.

Against a Node 24 server with keepAliveTimeout = 1000 we saw 0 failures with all three clients, even with keepAliveTimeoutBuffer = 0. The Node server sends Keep-Alive: timeout=1, and the clients dropped the socket before reusing it (reusedSocket was false every time). Also, Node 24 servers have keepAliveTimeoutBuffer = 1000 by default: with keepAliveTimeout = 1000 the server really closed the idle socket after 2002 ms. More on the header in Keep-Alive and Connection headers.

6. How to fix it

const agent = new http.Agent({ keepAlive: true, timeout: 50_000 }); // server/LB idle is 60 s

function get(url, retried = false) {
  return new Promise((resolve, reject) => {
    const req = http.get(url, { agent }, resolve);
    req.on('error', err => {
      if (!retried && req.reusedSocket && err.code === 'ECONNRESET') {
        return resolve(get(url, true));   // stale socket: retry once
      }
      reject(err);
    });
  });
}

For fetch, undici's Agent has its own keepAliveTimeout option (per the undici docs, not measured here). For the server-side view of resets, read ERR_CONNECTION_RESET / ECONNRESET. Behind a proxy, the same race shows up as 502 Bad Gateway.

Frequently asked questions

Why does Node say ECONNRESET when the server closed cleanly?

The http client uses code ECONNRESET for every 'socket hang up', whether the close was a FIN or a real reset. In our tests a clean socket.destroy() on the server gave 'socket hang up' with code ECONNRESET.

Why does fetch not show 'socket hang up'?

Built-in fetch uses undici, not the http module. It throws 'TypeError: fetch failed' and the reason is in err.cause, for example 'SocketError: other side closed' with code UND_ERR_SOCKET. Always log err.cause.

Can my own code cause socket hang up?

Yes. Calling req.destroy() before the response, often inside a 'timeout' handler, gives the exact same 'socket hang up' ECONNRESET error. Pass an error to destroy() so your logs show the real reason.

Why do I get random socket hang up errors under load?

Most likely a stale keep-alive socket: the server or a proxy closed an idle connection as the client reused it. We reproduced it in a 2-3 ms window. Set the client agent's idle timeout lower than the server's and retry idempotent requests once when req.reusedSocket is true.

Does the http.request timeout option abort the request?

No. In our test, timeout: 500 with no 'timeout' handler still waited and returned 200 after 3 s. You must call req.destroy() in the 'timeout' handler yourself.

Written with AI assistance. Measured on 2026-10-02 with Node.js 24.21.0 (http.request, built-in fetch/undici), axios 1.20.0 and curl 8.14.1 against local Node and raw TCP test servers. Lines marked as from the docs were not measured. PlainHeaders is our extension; Node.js, axios and curl are free.