Node.js 26.6: TCP-сокеты уходят в worker threads
3 августа 2026 года вышел Node.js 26.6.0, и одна строка в его changelog тихо переносит архитектурную границу: net.Server и net.Socket теперь можно передавать между worker threads. Переезжает само живое сетевое соединение — не копия его байтов — из одного потока в другой внутри одного процесса.
Проблема: одно ядро работает, остальные смотрят
JavaScript в Node.js исполняется в одном потоке. На машине восемь ядер или шестнадцать, а процесс занимает ровно одно. Традиционный ответ — модуль cluster: поднять несколько отдельных процессов и дать системе распределять входящие соединения между ними.
Это работает, и цена известна каждому, кто держал такую схему в продакшене. У каждого процесса своя куча памяти, свой пул соединений с базой, своя копия любого кэша в памяти. Общее состояние требует внешнего слоя вроде Redis. Обмен между процессами идёт через IPC, а значит — сериализация каждого сообщения в байты и сборка обратно на другой стороне.
Worker threads предложили альтернативу ещё несколько лет назад: потоки внутри одного процесса, общая память через SharedArrayBuffer, без накладных расходов на отдельные процессы. Но оставался барьер — сеть. Соединение, принятое в главном потоке, было к нему привязано. Оставалось только копировать данные в worker и копировать результат обратно.
Что изменилось
Аргумент transferList у postMessage() теперь принимает два новых типа: net.Server и net.Socket. Официальная документация перечисляет их рядом с ArrayBuffer, MessagePort и FileHandle.
Отсюда два практических сценария:
- Передать сам server. Слушающий сокет вместе с соединениями, уже стоящими в очереди приёма, переезжает в event loop принимающего потока и продолжает принимать там.
- Передавать соединения по одному. Принимать в одном потоке и раздавать каждое соединение пулу воркеров. Это схема балансировщика, и она полезнее на практике.
Практический пример
Главный файл принимает и распределяет:
const net = require('node:net');
const { Worker } = require('node:worker_threads');
const pool = Array.from({ length: 4 }, () => new Worker('./worker.js'));
let next = 0;
const server = net.createServer((socket) => {
const worker = pool[next++ % pool.length];
// Соединение действительно переезжает — без копии и без сериализации
worker.postMessage({ socket }, [socket]);
});
server.listen(8000);Worker получает живое соединение и работает с ним так, будто принял его сам:
const { parentPort } = require('node:worker_threads');
const http = require('node:http');
const app = http.createServer((req, res) => {
res.end('handled inside a worker thread\n');
});
parentPort.on('message', ({ socket }) => {
app.emit('connection', socket);
});Вся суть — в строке worker.postMessage({ socket }, [socket]). Сокет не клонируется и не сериализуется; владение низкоуровневым handle переходит другому потоку.
Ограничения, которые стоит знать заранее
- Только Unix-подобные системы. На Windows
postMessage()бросаетERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED. Командам, которые разрабатывают на Windows и деплоят на Linux, нужен запасной путь. - Только TCP. Ни UDP, ни pipes.
- Соединение должно быть свежим. Не начало чтение, без буферизованных данных, не в процессе подключения и не уничтожено. Иначе —
ERR_WORKER_HANDLE_NOT_TRANSFERABLE. На практике: передавайте его прямо внутри колбэка соединения, до того как навесите слушателей. - Передача — это перенос, а не совместное владение. На отправляющей стороне сокет уничтожается, дальнейшее обращение бросает
ERR_STREAM_DESTROYED, а не молча теряет данные — осознанное и правильное решение. - Node.js 26 — это Current, а не LTS. Для продакшена сегодня актуальна линия 24. Тем, кто уже на 26, стоит следить и за недавними правками безопасности в permission model.
- Изоляция отказов остаётся за `cluster`. Отдельные процессы локализуют падения, потоки разделяют судьбу. Упавший worker может утянуть за собой весь процесс.
Вывод
Выбор больше не между одним ядром и многими. Он между двумя формами параллелизма. cluster даёт изоляцию отказов, поддержку всех платформ и стабильность LTS уже сегодня. Worker threads с передаваемыми сокетами дают одну общую кучу, один кэш, один пул к базе и никакого налога на сериализацию посередине.
Это не сноска в changelog. Это новая граница того, что можно спросить с одного процесса Node.js.
Первоисточник: документация Node.js по net и release notes v26.6.0.