Skip to content

http: Server.closeIdleConnections() does not iterate pre-request sockets #63452

@cyco130

Description

@cyco130

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions