MessageChannel
Creates a pair of linked ports forming a two-way communication channel between threads.
new MessageChannel()- Added in Node.js v10.5.0
Return value
<MessageChannel>An object with port1 and port2 properties, which refer to two linked MessagePort instances.
Errors
Nothing. The constructor takes no arguments and cannot fail under normal conditions.
new MessageChannel() yields { port1, port2 }: two MessagePort objects wired to each other. Anything posted on one arrives as a 'message' event on the other. The class itself has no methods; it exists purely to mint the pair. The API mirrors the browser's MessageChannel, so the same code shape works in both places.
When to reach for it
The built-in parentPort channel is a single lane between a worker and its creator. Reach for a MessageChannel when you want more:
- Thread-to-thread links. Create the pair on the main thread, send
port1to worker A andport2to worker B, and they talk directly without the main thread relaying every message. - Separate lanes. One channel for control messages, one for bulk data, so a large payload never delays a cancellation.
- Per-request replies. Send a fresh port with each job and close it when the answer arrives, instead of correlating ids on one shared channel.
What trips people up
A port must be transferred, never cloned: pass it in the transferList argument of postMessage() (or the transferList option of new Worker()). Omit it and you get ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST. Once transferred, the port is unusable in the sending thread. An open port with a listener also keeps its thread alive, so close both ends when finished.
Example
const { MessageChannel } = require('node:worker_threads');
const { port1, port2 } = new MessageChannel();
port1.on('message', (message) => {
console.log('port1 received:', message);
port1.close();
port2.close();
});
port2.postMessage({ foo: 'bar' });
// Prints:
// port1 received: { foo: 'bar' }When to use it
- Wire two worker threads together so data does not relay through the main thread
- Keep a separate control channel for cancellation alongside a busy data channel
- Hand each request its own short-lived reply port instead of correlating ids
- Give a worker a port into a shared logging or metrics thread
Frequently asked questions
How do two worker threads communicate without going through the main thread?
Create a MessageChannel on the main thread and transfer port1 to one worker and port2 to the other, then post directly between them. The ports must appear in the transferList of the postMessage call or the Worker constructor, otherwise the transfer throws.
Why do I get ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST?
Because a MessagePort was included in the message or in workerData without being listed in transferList. Ports cannot be structured-cloned — they can only be moved — so every port in a payload must also be named as transferable.
What is the difference between MessageChannel and BroadcastChannel?
A MessageChannel connects exactly two endpoints and its ports can be transferred anywhere, while BroadcastChannel delivers each message to every thread that has joined the same named channel. Use MessageChannel for point-to-point work distribution and BroadcastChannel for fan-out notifications such as shutdown signals.
More examples
Letting two workers talk to each other directly
The three words never pass through the main thread — it only creates the pair and hands one end to each worker. Both transferList entries are mandatory; dropping either one makes the Worker constructor throw ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST. The explicit null end marker exists because a channel has no built-in end-of-stream signal.
// main.js
const { Worker, MessageChannel } = require('node:worker_threads');
const { port1, port2 } = new MessageChannel();
const producer = new Worker('./producer.js', {
workerData: { port: port1 },
transferList: [port1], // required: ports are transferred, not cloned
});
const consumer = new Worker('./consumer.js', {
workerData: { port: port2 },
transferList: [port2],
});
consumer.on('message', (msg) => {
console.log('main saw:', msg);
Promise.all([producer.terminate(), consumer.terminate()]);
});
// producer.js
const { workerData } = require('node:worker_threads');
for (const word of ['alpha', 'beta', 'gamma']) {
workerData.port.postMessage(word);
}
workerData.port.postMessage(null); // end marker
// consumer.js
const { parentPort, workerData } = require('node:worker_threads');
const seen = [];
workerData.port.on('message', (word) => {
if (word === null) {
workerData.port.close();
parentPort.postMessage(`consumed ${seen.length}: ${seen.join(', ')}`);
return;
}
seen.push(word);
});
// Prints:
// main saw: consumed 3: alpha, beta, gammaA reply port per request
Each request carries its own private reply channel, so there is no need to tag messages with ids or to keep a pending-request map on either side. Because port1 uses once() and both ends are closed after the reply, no listener or port leaks between calls — the failure mode this avoids is a growing set of 'message' handlers on one shared channel.
// main.js
import { Worker, MessageChannel } from 'node:worker_threads';
const worker = new Worker('./echo.js');
function ask(text) {
const { port1, port2 } = new MessageChannel();
return new Promise((resolve) => {
port1.once('message', (reply) => {
port1.close();
resolve(reply);
});
worker.postMessage({ text, reply: port2 }, [port2]);
});
}
console.log(await ask('one'));
console.log(await ask('two'));
await worker.terminate();
// echo.js
import { parentPort } from 'node:worker_threads';
parentPort.on('message', ({ text, reply }) => {
reply.postMessage(text.toUpperCase());
reply.close();
});
// Prints:
// ONE
// TWO
Discussion