worker.postMessage()
Sends a structured-cloned value from the parent thread into a running worker.
worker.postMessage(value[, transferList])- Added in Node.js v10.5.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$value | <any> | The value to send. It is copied using the HTML structured clone algorithm, so functions, class prototypes and closures do not survive the trip. |
$transferListoptional | <Object[]> | Objects whose underlying memory is moved into the worker instead of copied, such as ArrayBuffer, MessagePort and FileHandle. Transferred objects become unusable in the sending thread. |
Return value
<undefined>Nothing. Delivery is asynchronous; the message arrives on the worker's parentPort 'message' event.
Errors
Throws a DataCloneError if value contains something the structured clone algorithm cannot copy, such as a function; throws ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST if a MessagePort is sent without being listed in transferList; and since v21.0.0 throws if transferList contains an untransferable object.
Changelog
- PHP v21.0.0An error is thrown when an untransferable object appears in the transfer list, rather than the object being silently ignored.
- PHP v15.14.0, v14.18.0BlockList was added to the list of cloneable types.
- PHP v15.9.0, v14.18.0Histogram types were added to the list of cloneable types.
- PHP v15.6.0X509Certificate was added to the list of cloneable types.
- PHP v15.0.0CryptoKey was added to the list of cloneable types.
- PHP v14.5.0, v12.19.0KeyObject was added to the list of cloneable types, and FileHandle to the list of transferable types.
This is the parent's half of the conversation. Where workerData delivers a payload once at startup, postMessage() can be called any number of times while the thread runs, and the worker sees each call as a 'message' event on parentPort. Delivery is asynchronous and the call returns immediately, so there is no return value and no promise — a reply comes back separately on worker.on('message').
When to reach for it
Whenever a thread outlives a single job: dispatching work to a pool, streaming chunks into a decoder, sending a cancel or shutdown signal, pushing updated configuration.
What trips people up
- Copying is not free. A large object is serialised on the way out and deserialised on the way in, on both event loops. Above a few hundred kilobytes, put the payload in an
ArrayBufferand list it intransferList— that moves the memory instead of copying it. - Transfer is destructive. A transferred
ArrayBufferis detached in the parent; touching it afterwards yields a zero-length buffer. - A busy thread will not hear you. Messages are queued and only read when the worker returns to its event loop, so they cannot interrupt a synchronous loop. Use
worker.terminate()for that. - Messages sent after the worker has exited are silently discarded.
Example
const { Worker, isMainThread, parentPort } = require('node:worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
worker.on('message', (result) => {
console.log('result:', result);
if (result.op === 'stop') worker.terminate();
});
worker.postMessage({ op: 'sum', values: [1, 2, 3, 4] });
worker.postMessage({ op: 'max', values: [7, 2, 9] });
worker.postMessage({ op: 'stop' });
} else {
parentPort.on('message', (job) => {
if (job.op === 'sum') {
parentPort.postMessage({ op: 'sum', value: job.values.reduce((a, b) => a + b, 0) });
} else if (job.op === 'max') {
parentPort.postMessage({ op: 'max', value: Math.max(...job.values) });
} else {
parentPort.postMessage({ op: 'stop' });
}
});
}
// Prints:
// result: { op: 'sum', value: 10 }
// result: { op: 'max', value: 9 }
// result: { op: 'stop' }When to use it
- Dispatch jobs to a long-lived pool of worker threads
- Stream file or network chunks into a parsing thread as they arrive
- Hand a large pixel or audio buffer to a thread with zero copying
- Push a shutdown or configuration-reload signal to a running thread
Frequently asked questions
How do I send data to a worker thread after it has started?
Call worker.postMessage(value) on the Worker object in the parent; the thread receives it via parentPort.on('message'). Use the workerData constructor option instead when the data is only needed once, at startup.
Why is my ArrayBuffer empty after posting it to a worker?
Because it was listed in transferList, which moves the memory rather than copying it and detaches the buffer in the sending thread, leaving byteLength 0. Drop it from transferList to send a copy, or have the worker transfer the buffer back when it is done.
Why does my worker ignore a message while it is busy?
Messages are queued and only delivered when the worker returns to its event loop, so nothing can interrupt a synchronous loop. Poll a SharedArrayBuffer flag with Atomics inside the loop for cooperative cancellation, or call worker.terminate() to stop the thread outright.
More examples
Transferring a buffer instead of copying it
The byteLength dropping to 0 is the whole point: the buffer was detached in the parent and its memory now belongs to the worker, with no copy made regardless of size. Ownership has to be handed back the same way if the parent wants the data again. Reading the detached buffer in between is not an error — it just looks empty, which is why this bug is usually found late.
// main.js
const { Worker } = require('node:worker_threads');
const worker = new Worker('./invert.js');
const buffer = new ArrayBuffer(8);
new Uint8Array(buffer).set([1, 2, 3, 4, 5, 6, 7, 8]);
console.log('before transfer, byteLength:', buffer.byteLength);
worker.postMessage({ pixels: buffer }, [buffer]);
console.log('after transfer, byteLength:', buffer.byteLength);
worker.on('message', (back) => {
console.log('returned:', Array.from(new Uint8Array(back)));
worker.terminate();
});
// invert.js
const { parentPort } = require('node:worker_threads');
parentPort.on('message', ({ pixels }) => {
const view = new Uint8Array(pixels);
for (let i = 0; i < view.length; i++) view[i] = 255 - view[i];
parentPort.postMessage(pixels, [pixels]); // hand ownership back
});
// Prints:
// before transfer, byteLength: 8
// after transfer, byteLength: 0
// returned: [ 254, 253, 252, 251, 250, 249, 248, 247 ]Cancelling with a shared flag, not a message
This shows the limit of postMessage: the worker is inside a synchronous loop, so a second posted message would queue behind it and only be read after the loop ended. A SharedArrayBuffer checked with Atomics.load is visible immediately, giving cooperative cancellation. The message text avoids printing the iteration count because that varies from run to run.
// main.js
const { Worker } = require('node:worker_threads');
const control = new Int32Array(new SharedArrayBuffer(4));
const worker = new Worker('./scan.js', { workerData: { control } });
worker.on('message', (msg) => console.log(msg));
// A posted message would sit in the queue until the loop below finished.
// A shared flag is visible to the worker mid-loop.
worker.postMessage({ op: 'start', limit: 1e9 });
setTimeout(() => Atomics.store(control, 0, 1), 50);
// scan.js
const { parentPort, workerData } = require('node:worker_threads');
const { control } = workerData;
parentPort.on('message', ({ limit }) => {
let i = 0;
for (; i < limit; i++) {
if ((i & 0xffff) === 0 && Atomics.load(control, 0) === 1) break;
}
parentPort.postMessage(`stopped after ${i > 0 ? 'partial' : 'no'} work`);
parentPort.close();
});
// Prints:
// stopped after partial work
Discussion