worker_threads
require('node:worker_threads') The node:worker_threads module is the nodejs standard library’s answer to JavaScript multithreading. Each worker runs on its own V8 thread with a separate event loop, which is the shared-memory concurrency model Node has shipped since v10.5. Reach for it when a CPU-bound task (parsing, crypto, image processing, compression, large JSON transforms) blocks the main event loop. It does not help with I/O; Node’s async APIs already cover that. Unlike child_process, workers can share memory: ArrayBuffer instances can be transferred, and SharedArrayBuffer instances are shared in place.
This page covers every export in the module: the Worker class, MessageChannel, MessagePort, BroadcastChannel, the constants exposed inside a worker, and the helper functions available from any thread. Together they cover the four common nodejs concurrency patterns: a pool of workers for CPU-bound tasks, a MessageChannel for direct worker-to-worker hand-off, BroadcastChannel for pub/sub, and SharedArrayBuffer for live shared memory.
Importing the module
Use the node: prefix. The unprefixed name also works for backward compatibility.
// ESM
import {
Worker,
isMainThread,
parentPort,
workerData,
threadId,
SHARE_ENV,
} from 'node:worker_threads';
// CJS
const {
Worker,
isMainThread,
parentPort,
workerData,
threadId,
SHARE_ENV,
} = require('node:worker_threads');
A worker has its own V8 isolate and event loop, but the underlying thread shares memory with the parent process. Plain data sent across postMessage is structured-cloned, not shared. To actually share, transfer an ArrayBuffer (the original becomes detached) or use a SharedArrayBuffer (both sides see the same bytes).
Inside a worker: module-level constants
These values are exposed as module exports but are meaningful only inside a worker thread. On the main thread they are either false, null, or undefined.
isMainThread
true when the current code is running on the main thread, false inside a Worker. The canonical pattern is if (isMainThread) { /* parent code */ } else { /* worker code */ } at the top of a shared file, so the same source can run on both sides.
parentPort
A MessagePort connected to the thread that spawned this worker, or null on the main thread. Use parentPort.postMessage(value) to send to the parent and parentPort.on('message', ...) to receive. parentPort is also an EventTarget, so .on(), .once(), and .emit() work.
workerData
The cloned value of the workerData option passed to the constructor. It is available immediately at module load time, which makes it the right channel for bootstrap configuration that the worker needs before it starts handling messages.
threadId
A unique integer for the current thread. Matches worker.threadId on the parent side. Useful for logging and as the target of postMessageToThread.
threadName
The custom name string from WorkerOptions.name, or null if none was set. Added in Node 24.6.
resourceLimits
The ResourceLimits object from the constructor (or defaults). See the ResourceLimits shape in the Worker section below.
SHARE_ENV
A unique symbol. Pass it as the env option to the Worker constructor to make the worker share process.env with the parent, so changes in one are visible in the other. Without it, the worker gets a copy and edits stay local.
isInternalThread
true when running inside Node’s own internal worker, such as the loader thread. Useful for libraries that need to avoid setting up their own workers inside Node’s bootstrap path. Added in Node 23.7 / 22.14.
The cleanest way to use the constants above is inside a single file that runs on both sides. Branch on isMainThread at the top, construct a Worker on the parent side, and have the worker reply through parentPort using the value cloned into workerData.
const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads');
if (isMainThread) {
const worker = new Worker(__filename, { workerData: 7 });
worker.once('message', (result) => console.log(result)); // 49
worker.postMessage('go');
} else {
parentPort.once('message', () => {
parentPort.postMessage(workerData * workerData);
});
}
The Worker class
new Worker(filename, options?) starts a new thread. filename must be an absolute path, a ./relative or ../relative path, or a file: URL. Under ESM, use new Worker(new URL('./worker.js', import.meta.url)) because __filename does not exist there.
WorkerOptions
| Option | Type | Default | Purpose |
|---|---|---|---|
argv | any[] | [] | Values stringified and appended to process.argv in the worker. |
env | Record<string, string> | typeof SHARE_ENV | copy of parent’s process.env | Use SHARE_ENV to share instead of copy. |
eval | boolean | false | If true, filename is a source string, not a path. |
workerData | any | undefined | Cloned and exposed as workerData inside the worker. |
stdin | boolean | false | If true, exposes worker.stdin as a writable stream. |
stdout | boolean | false | If true, process.stdout output is buffered into worker.stdout. |
stderr | boolean | false | Same, for process.stderr. |
execArgv | string[] | inherits | Node CLI flags passed to the worker (e.g. --inspect). |
resourceLimits | ResourceLimits | none | See below. |
transferList | Transferable[] | [] | Extra items to transfer with the first worker message. |
trackUnmanagedFds | boolean | true | Set false if the worker manages its own file descriptors. |
name | string | undefined | Human-readable label appended to [worker <id>]. |
ResourceLimits is { maxYoungGenerationSizeMb, maxOldGenerationSizeMb, codeRangeSizeMb, stackSizeMb }. The stackSizeMb default is 4; setting it too low makes the worker unusable.
Worker properties
worker.stdin (Writable or null), worker.stdout and worker.stderr (Readable) for piping; worker.threadId (number), worker.threadName (string or null), worker.resourceLimits (object), and worker.performance (with eventLoopUtilization).
Worker methods
worker.postMessage(value, transferList?) sends a message to the worker via its built-in parent port. worker.terminate() returns a Promise<number> for the exit code. worker.ref() and worker.unref() mirror the same calls on handles: unref() lets the process exit while the worker is still running. worker.getHeapSnapshot() returns a Promise<Readable> of a V8 heap snapshot. worker.getHeapStatistics() returns a Promise<HeapInfo>. worker.startCpuProfile() and worker.startHeapProfile() start profiling in Node 24+. worker[Symbol.asyncDispose]() calls terminate(); use it with await using (Node 24.2+).
Worker events
| Event | Payload | Emitted when |
|---|---|---|
'online' | — | The worker thread has started executing JS. |
'message' | value | A message arrived from the worker. |
'messageerror' | Error | A message was received but could not be deserialized. |
'error' | unknown | An uncaught exception escaped the worker. |
'exit' | exitCode | The worker stopped. Always emitted, even if 'error' was the cause. |
Listen for 'exit' (not just 'error') if you need to know the worker actually finished. Treat any non-zero exit code as a real failure.
MessageChannel
new MessageChannel() returns { port1, port2 }. Each is a MessagePort. Hand one port to one thread and the other to another, and they can exchange messages directly without routing through the parent.
const { Worker, MessageChannel, MessagePort, isMainThread, parentPort } =
require('node:worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
const { port1, port2 } = new MessageChannel();
worker.postMessage({ hereIsYourPort: port1 }, [port1]);
port2.on('message', (msg) => console.log('main received:', msg));
} else {
parentPort.once('message', ({ hereIsYourPort }) => {
hereIsYourPort.postMessage('hello from worker');
hereIsYourPort.close();
});
}
port1 becomes detached on the side that transfers it, so it cannot be used after postMessage.
MessagePort
A bidirectional messaging endpoint. port.postMessage(value, transferList?) sends; the transferList is required if value contains a MessagePort (otherwise Node throws ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST). port.start() begins dispatching queued messages, port.close() detaches the port, and port.ref() / port.unref() control whether the port keeps the event loop alive. Events: 'message', 'messageerror', 'close'. The Transferable union is ArrayBuffer | MessagePort | AbortSignal | FileHandle | ReadableStream | WritableStream | TransformStream.
BroadcastChannel
A pub/sub channel scoped to the current process. Every BroadcastChannel with the same name (across threads) receives every message posted to any of them.
const { Worker, BroadcastChannel, isMainThread, workerData } =
require('node:worker_threads');
if (isMainThread) {
const ch = new BroadcastChannel('jobs');
for (let i = 0; i < 3; i++) new Worker(__filename, { workerData: i });
setTimeout(() => ch.postMessage('shutdown'), 100);
} else {
const ch = new BroadcastChannel('jobs');
ch.onmessage = (ev) => console.log(`worker ${workerData} got:`, ev.data);
}
Note that BroadcastChannel is process-local. Two separate Node processes cannot reach each other through one.
Module functions
Available from any thread.
setEnvironmentData(key, value?) and getEnvironmentData(key) propagate small, serializable config across the worker boundary. The data is cloned per worker, so later mutations in one worker are not seen in others.
postMessageToThread(threadId, value, transferList?, timeout?) sends a one-shot message to any thread that has registered a process.on('workerMessage', ...) handler. The target throws ERR_WORKER_MESSAGING_FAILED if it has not. Use this for non-parent/child threads; for direct parent/child communication, use parentPort or worker.postMessage.
markAsUntransferable(object) and isMarkedAsUntransferable(object) (Node 21+) mark an object so postMessage will not transfer it, even if it is a Transferable. Useful for protecting shared Buffer pools.
markAsUncloneable(object) (Node 22.10+) prevents the object from being structured-cloned. Throws if you try to postMessage it.
moveMessagePortToContext(port, contextifiedSandbox) moves a MessagePort into a vm context.
receiveMessageOnPort(port) is the only synchronous way to read from a port. It pulls one message off the queue and returns { message } or undefined if the queue is empty. It does not emit a 'message' event, which is why it is useful in tight loops that want to avoid event-loop overhead.
Sharing memory: transfer vs. SharedArrayBuffer
Transfer moves ownership of the bytes. The original ArrayBuffer is detached and its byteLength becomes 0.
const { Worker } = require('node:worker_threads');
const buf = new ArrayBuffer(8);
const view = new Uint8Array(buf);
view[0] = 1;
const worker = new Worker(
`const { parentPort } = require('node:worker_threads');
parentPort.on('message', (u8) => {
u8[0] = 99;
parentPort.postMessage('done');
});`,
{ eval: true }
);
worker.postMessage(view, [view.buffer]); // transfer
console.log(view.byteLength); // 0
worker.on('message', console.log);
SharedArrayBuffer is shared in place, so both threads read and write the same bytes. The classic producer-consumer pattern uses Atomics.wait on the consumer side to block cheaply until a value lands, and Atomics.notify on the producer side to wake it. That avoids the need for locks or polling loops.
const { Worker } = require('node:worker_threads');
const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);
new Worker(
`const { workerData } = require('node:worker_threads');
const view = new Int32Array(workerData);
Atomics.wait(view, 0, 0);
const v = Atomics.load(view, 0);
Atomics.store(view, 0, v * 2);`,
{ eval: true, workerData: sab }
);
Atomics.store(view, 0, 1);
Atomics.notify(view, 0, 1);
Common patterns
Use a pool. Creating a worker has overhead. The official docs call this out: spinning a fresh worker per task is usually slower than running the work inline. Pair the pool with AsyncResource from node:async_hooks so async-stack traces correlate correctly.
Use await using in Node 24+. await using worker = new Worker(...) calls worker[Symbol.asyncDispose]() (which is terminate()) when the scope exits. Cleaner than wrapping the lifetime in a try/finally.
Use unref() for fire-and-forget background work. If a worker is doing cleanup that should not keep the process alive, call worker.unref() and the process can exit on its own.
Share process.env deliberately. The default copy hides the worker’s process.env.FOO = '...' from the parent. Use env: SHARE_ENV only when you actually want that, and remember that on Windows the worker copy is case-sensitive even though the parent’s is not.
Keep Buffers in a pool when transferring. If you transfer Buffers out of a pool, mark the pool with markAsUntransferable so a stray transfer does not corrupt the pool.
See Also
- Events —
WorkerandMessagePortextendEventTarget;.on(),.once(), and.emit()are inherited. - ArrayBuffer — the
Transferabletype used for memory transfer. - Process —
process.env,process.argv, and theprocess.exit/process.abortquirks inside workers.