worker_threads: proposal for Persistent Worker Runtime with stateful workers and outbox dispatch
What is the problem this feature will solve?
Node.js' single-threaded Event Loop excels at asynchronous non-blocking I/O. However, synchronous CPU-bound operations (cryptography, AST/JSON parsing, image manipulation, template rendering, ML embeddings) freeze the main Event Loop, leading to latency spikes and request timeouts.
Currently, developers face several limitations when trying to offload work:
- Manual
new Worker()per task induces high memory allocation and V8 Isolate bootstrap latency (~20ms per task). - Existing userland worker pools (
piscina,workerpool) treat worker threads strictly as disposable, stateless function runners, discarding in-memory worker heap state between tasks. - In-process background tasks & side-effects (e.g. the Transactional Outbox pattern: sending an email or webhook after writing to the DB) have no standard core abstraction. Developers either do naive fire-and-forget on the main thread (risking Event Loop blocking during template compilation and unhandled promise crashes) or are forced to add external infrastructure like Redis + BullMQ / RabbitMQ just for simple in-process background jobs.
In our empirical benchmarks, executing CPU tasks synchronously on the main thread froze the Event Loop for 2,207ms, completely starving incoming I/O during computation.
What is the feature you are proposing to solve the problem?
We propose adding a built-in Persistent Worker Runtime to Node.js (either as node:worker_runtime or as an enhancement under node:worker_threads), operating on the core axiom:
"The Event Loop coordinates. Persistent Workers execute."
Core Capabilities:
- Dual Execution Model:
runtime.execute(task): Interactive request-response mode. Offloads computation and awaits the result with near-zero Event Loop lag (~6.5ms in benchmarks).runtime.dispatch(task): Background dispatch mode for Transactional Outbox / fire-and-confirm patterns. Returns an unblockedTaskHandleimmediately, allowing sub-millisecond HTTP responses while exposing.onComplete(cb)and.onError(cb)for asynchronous confirmation.
- Bounded Concurrency Batch Execution (
executeAll):- Native
Promise.allsemantics, but bounded strictly to the worker pool's hardware capacity (e.g., 4 workers processing 40 tasks in parallel without CPU thrashing).
- Native
- Stateful Workers with Persistent L1 Heap Memory:
- Dedicated workers can retain warm in-memory heaps (caches, loaded models, compiled WASM modules) across tasks with worker affinity. Benchmarks show a 33.2x latency speedup (1.58ms vs 52.37ms per query) compared to stateless reloading.
- Diagnostic & Backpressure Integration:
- Native
AsyncResource(node:async_hooks) propagation to preserve OpenTelemetry / APM distributed tracing context across threads. - Non-blocking queue backpressure with timeout (
TaskQueueTimeoutError) to prevent process OOM. - Supervisor with automatic crash detection and worker isolate replacement.
- Native
- Zero-Copy Memory Transfer:
- Full support for
transferListto transferArrayBufferinstances in <0.1ms without memory copying.
- Full support for
Reference Implementation:
We have developed a functional, production-grade reference implementation written in pure vanilla JavaScript (ESM, zero external runtime dependencies) with 100% passing tests on Node.js 22/24, 9 Architecture Decision Records (ADRs), and empirical benchmarks: https://github.com/FelipeMiiller/persistent-worker-runtime
What alternatives have you considered?
- Userland libraries (
piscina,workerpool): While Piscina is excellent for stateless function execution, it lacks native support for persistent stateful workers with warm L1 memory affinity, in-process outbox dispatch with asynchronous event confirmation, and standard zero-dependency stdlib availability. - External Queues (Redis + BullMQ / RabbitMQ / SQS): Heavy operational and infrastructure overhead for applications that only require reliable in-process background job offloading.
libuvThreadpool (UV_THREADPOOL_SIZE): Only handles internal asynchronous I/O and specific C++ crypto/zlib operations; cannot be used to run arbitrary userland JavaScript code.- Child Processes (
child_process.fork): Full OS process sandboxing incurs 30-50MB RAM per process and higher IPC serialization latency compared to lightweight V8 Isolates with zero-copytransferList.
Source: nodejs/node