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
| Cause | http.request | fetch (undici) | axios 1.20.0 |
|---|---|---|---|
Server destroys socket without responding (req.socket.destroy()) | Error: socket hang up, code ECONNRESET | TypeError: fetch failed, cause SocketError: other side closed, code UND_ERR_SOCKET | Error: socket hang up, code ECONNRESET |
| Server process crashes mid-request (uncaught throw in handler) | socket hang up / ECONNRESET | fetch failed / other side closed / UND_ERR_SOCKET | socket 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-body | Error: aborted, code ECONNRESET (on the response) | TypeError: terminated, cause other side closed | AxiosError: stream has been aborted, code ERR_BAD_RESPONSE |
Server idle timeout (server.setTimeout(500), handler takes 3 s) | socket hang up / ECONNRESET at 503 ms | fetch failed / other side closed at 503 ms | socket hang up / ECONNRESET |
| Plain http:// to an HTTPS port | socket hang up / ECONNRESET (3 ms) | fetch failed / other side closed | socket hang up / ECONNRESET |
| https:// to a plain HTTP port | Error: write EPROTO ... wrong version number, code EPROTO | fetch failed, cause code ERR_SSL_WRONG_VERSION_NUMBER | write EPROTO ... wrong version number, code EPROTO |
| Stale keep-alive socket (see section 5) | socket hang up or read ECONNRESET, req.reusedSocket = true | fetch failed, cause read ECONNRESET | socket 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 ms | Error: 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' handler | No 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: 300 | AxiosError: timeout of 300ms exceeded, code ECONNABORTED |
axios signal aborted | CanceledError: 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.
| Client | Failures | When | Error |
|---|---|---|---|
| http.request, global agent | 2 of 31 | 998 and 1000 ms idle | read ECONNRESET and socket hang up, both with reusedSocket true |
| axios default | 2 of 31 | 998 and 1000 ms idle | read ECONNRESET and socket hang up |
| fetch | 0 of 31 in the sweep; 3 of 24 in a denser 994–1001 ms run | 997 ms idle | fetch failed, cause read ECONNRESET |
http.request, new http.Agent({keepAlive: true, timeout: 900}) | 0 of 24 | 994–1001 ms | None. 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
- Find out who closed. Check server logs for a crash or a timeout at the same time. A
ECONNREFUSEDright after a hang up means the server process died. - Client idle timeout below the server's. Set the agent's idle
timeoutlower than the server's (or proxy's, or load balancer's) keep-alive timeout. In our test,timeout: 900against a 1000 ms server removed every failure. - Retry idempotent requests once on a reused socket. The failures all had
req.reusedSocket === true. Retry GET, HEAD, PUT and DELETE in that case. Do not blindly retry POST. - Check the scheme and port. http:// to an HTTPS port is a hang up; https:// to an HTTP port is
EPROTO"wrong version number". - Handle client timeouts on purpose. Use
AbortSignal.timeout()with fetch, orreq.destroy(new Error('timeout'))with http, so your logs say "timeout" instead of "socket hang up". - Raise server timeouts for slow handlers (
server.setTimeout, proxy read timeouts), or make the endpoint asynchronous.
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.