Version
v24.15.0 (also tested on several other versions)
Platform
Darwin G0674 25.4.0 Darwin Kernel Version 25.4.0: Thu Mar 19 19:30:44 PDT 2026; root:xnu-12377.101.15~1/RELEASE_ARM64_T6000 arm64
(Also tested on Linux Mint)
Subsystem
http
What steps will reproduce the bug?
import { createServer } from "node:http";
import { createConnection } from "node:net";
const server = createServer((req, res) => res.end("ok"));
server.listen(3099, "127.0.0.1");
await new Promise((r) => server.once("listening", r));
// TCP socket that doesn't send anything
const sock = createConnection(3099, "127.0.0.1");
await new Promise((r) => sock.once("connect", r));
// Wait until the server has actually accepted and registered the socket,
await new Promise((r) => server.once("connection", r));
const closed = new Promise((r) =>
server.close(() => r(performance.now())),
);
const closeStart = performance.now();
server.closeIdleConnections();
console.log("after closeIdleConnections, awaiting close...");
const t = await Promise.race([
closed,
new Promise((r) => setTimeout(() => r(null), 2000)),
]);
console.log(t === null ? "still hung after 2s" : `closed in ${t - closeStart}ms`);
sock.destroy();
How often does it reproduce? Is there a required condition?
None.
What is the expected behavior? Why is that the expected behavior?
Server.closeIdleConnections() docs say:
Closes all connections connected to this server which are not sending a request or waiting for a response.
A TCP socket that is connected but has not yet sent any HTTP data fits the description to me. So the expected behavior would be closeIdleConnections() to close them:
after closeIdleConnections, awaiting close...
closed in <some number below 2000>ms
What do you see instead?
But pre-request sockets are not closed:
after closeIdleConnections, awaiting close...
still hung after 2s
Additional information
Browsers (Chrome especially) routinely open speculative TCP connections that don't send HTTP data until needed. They remain open for a while even when the user closes the tab or navigates away from the page. Server-side, those sockets are open with bytesRead === 0 and they block graceful shutdown in pretty much all frameworks with a graceful shutdown feature:
server.close() waits for them (as it should).
server.closeIdleConnections() is the documented escape hatch for "drop idle connections now," but per above it doesn't.
- The only working option is
server.closeAllConnections(), which also kills in-flight requests and defeats the purpose of graceful drain.
A quick search shows that it affects graceful shutdown in frameworks. See, for instance, Fastify #5713 (closed as "not planned").
A user-space workaround exists (track 'connection' events, drop sockets with bytesRead === 0) but there's at least a mismatch between the docs and the impl.
The root cause seems to be the implementation in lib/_http_server.js that iterates this[kConnections].idle():
Server.prototype.closeIdleConnections = function closeIdleConnections() {
if (!this[kConnections]) return;
const connections = this[kConnections].idle();
for (let i = 0, l = connections.length; i < l; i++) {
if (connections[i].socket._httpMessage && !connections[i].socket._httpMessage.finished) continue;
connections[i].socket.destroy();
}
};
kConnections is HTTP-level state; sockets are added when an HTTP exchange begins. Pre-request sockets exist in net.Server's connection list (server._connections) but never reach kConnections. So the HTTP-level bookkeeping doesn't see them at all.
It would be nice to see a proper Node.js-level fix, or, if it's not possible or desirable for whatever reason, a documentation update to warn against the caveat.
Version
v24.15.0 (also tested on several other versions)
Platform
Subsystem
http
What steps will reproduce the bug?
How often does it reproduce? Is there a required condition?
None.
What is the expected behavior? Why is that the expected behavior?
Server.closeIdleConnections() docs say:
A TCP socket that is connected but has not yet sent any HTTP data fits the description to me. So the expected behavior would be
closeIdleConnections()to close them:What do you see instead?
But pre-request sockets are not closed:
Additional information
Browsers (Chrome especially) routinely open speculative TCP connections that don't send HTTP data until needed. They remain open for a while even when the user closes the tab or navigates away from the page. Server-side, those sockets are open with
bytesRead === 0and they block graceful shutdown in pretty much all frameworks with a graceful shutdown feature:server.close()waits for them (as it should).server.closeIdleConnections()is the documented escape hatch for "drop idle connections now," but per above it doesn't.server.closeAllConnections(), which also kills in-flight requests and defeats the purpose of graceful drain.A quick search shows that it affects graceful shutdown in frameworks. See, for instance, Fastify #5713 (closed as "not planned").
A user-space workaround exists (track
'connection'events, drop sockets withbytesRead === 0) but there's at least a mismatch between the docs and the impl.The root cause seems to be the implementation in
lib/_http_server.jsthat iteratesthis[kConnections].idle():kConnectionsis HTTP-level state; sockets are added when an HTTP exchange begins. Pre-request sockets exist innet.Server's connection list (server._connections) but never reachkConnections. So the HTTP-level bookkeeping doesn't see them at all.It would be nice to see a proper Node.js-level fix, or, if it's not possible or desirable for whatever reason, a documentation update to warn against the caveat.