Skip to content

worker_threads: proposal for Persistent Worker Runtime with stateful workers and outbox dispatch #66071

Description

@FelipeMiiller

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:

  1. Manual new Worker() per task induces high memory allocation and V8 Isolate bootstrap latency (~20ms per task).
  2. Existing userland worker pools (piscina, workerpool) treat worker threads strictly as disposable, stateless function runners, discarding in-memory worker heap state between tasks.
  3. 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:

  1. 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 unblocked TaskHandle immediately, allowing sub-millisecond HTTP responses while exposing .onComplete(cb) and .onError(cb) for asynchronous confirmation.
  2. Bounded Concurrency Batch Execution (executeAll):
    • Native Promise.all semantics, but bounded strictly to the worker pool's hardware capacity (e.g., 4 workers processing 40 tasks in parallel without CPU thrashing).
  3. 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.
  4. 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.
  5. Zero-Copy Memory Transfer:
    • Full support for transferList to transfer ArrayBuffer instances in <0.1ms without memory copying.

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?

  1. 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.
  2. External Queues (Redis + BullMQ / RabbitMQ / SQS):
    Heavy operational and infrastructure overhead for applications that only require reliable in-process background job offloading.
  3. libuv Threadpool (UV_THREADPOOL_SIZE):
    Only handles internal asynchronous I/O and specific C++ crypto/zlib operations; cannot be used to run arbitrary userland JavaScript code.
  4. 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-copy transferList.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature requestIssues requesting new Node.js features.

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions