From 7aa2718686094d244739dcce0d17dbf03dba7d37 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 1 Jul 2026 09:51:42 +0200 Subject: [PATCH 1/4] net: make TCP Server and Socket transferable across worker threads Allow a listening net.Server or an accepted net.Socket to be moved to another thread by listing it in the transferList of a worker_threads postMessage() call. Unix only; Windows throws. Signed-off-by: Matteo Collina --- doc/api/errors.md | 16 +++ doc/api/net.md | 40 ++++++ doc/api/worker_threads.md | 21 +++- lib/internal/errors.js | 6 + lib/net.js | 119 ++++++++++++++++++ src/tcp_wrap.cc | 90 +++++++++++++ src/tcp_wrap.h | 28 +++++ .../test-net-server-transfer-worker.js | 60 +++++++++ .../test-net-socket-transfer-worker.js | 59 +++++++++ test/parallel/test-net-transfer-guards.js | 52 ++++++++ ...-transfer-fake-js-transferable-internal.js | 9 +- 11 files changed, 492 insertions(+), 8 deletions(-) create mode 100644 test/parallel/test-net-server-transfer-worker.js create mode 100644 test/parallel/test-net-socket-transfer-worker.js create mode 100644 test/parallel/test-net-transfer-guards.js diff --git a/doc/api/errors.md b/doc/api/errors.md index 07d439da8e17ed..fa4eb436803e5c 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -3564,6 +3564,22 @@ added: v18.1.0 The `Response` that has been passed to `WebAssembly.compileStreaming` or to `WebAssembly.instantiateStreaming` is not a valid WebAssembly response. + + +### `ERR_WORKER_HANDLE_NOT_TRANSFERABLE` + +An attempt was made to transfer a `net.Socket` or `net.Server` to another thread +via a `worker_threads` `postMessage()` call while it was not in a transferable +state, for example because it had already started reading or had buffered data. + + + +### `ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED` + +An attempt was made to transfer a `net.Socket` or `net.Server` to another thread +on a platform where moving the underlying handle between event loops is not +supported (currently Windows). + ### `ERR_WORKER_INIT_FAILED` diff --git a/doc/api/net.md b/doc/api/net.md index e2f06776409898..3f6e095a2ccb0b 100644 --- a/doc/api/net.md +++ b/doc/api/net.md @@ -305,6 +305,11 @@ added: v0.1.90 This class is used to create a TCP or [IPC][] server. +A listening TCP `net.Server` can be transferred to a worker thread by listing it +in the `transferList` of a [`worker_threads`][] `postMessage()` call. This moves +the underlying listening socket to the receiving thread, where it resumes +accepting connections. See [Transferring TCP handles to other threads][]. + ### `new net.Server([options][, connectionListener])` * `options` {Object} See @@ -751,6 +756,39 @@ is received. For example, it is passed to the listeners of a [`'connection'`][] event emitted on a [`net.Server`][], so the user can use it to interact with the client. +### Transferring TCP handles to other threads + +A connected TCP `net.Socket` can be moved to another thread by listing it in the +`transferList` of a [`worker_threads`][] `postMessage()` call. After the +transfer, the socket is no longer usable on the sending thread and continues to +work on the receiving thread. This makes it possible to accept connections on +one thread and distribute them across a pool of worker threads, for example to +build a `node:cluster`-like model on top of worker threads. + +The socket must be a freshly accepted or created TCP connection: it must still +be attached to a live handle, must not be connecting or destroyed, and must not +have started reading or have buffered data. Otherwise `postMessage()` throws +`ERR_WORKER_HANDLE_NOT_TRANSFERABLE`. Only TCP sockets are supported, and only +on Unix-like platforms; on Windows `postMessage()` throws +`ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED`. + +```cjs +const net = require('node:net'); +const { Worker } = require('node:worker_threads'); + +// worker.js receives `{ socket }` messages and handles each connection. +const worker = new Worker('./worker.js'); + +const server = net.createServer((socket) => { + // Hand the freshly accepted connection off to the worker thread. + worker.postMessage({ socket }, [socket]); +}); +server.listen(8000); +``` + +A listening [`net.Server`][] can be transferred the same way, which moves the +listening socket itself (and its pending accept queue) to the receiving thread. + ### `new net.Socket([options])`