O MessageChannel no Node.js cria dois canais conectados para troca de mensagens entre partes da aplicação. Cada extremidade é representada por um MessagePort, permitindo enviar objetos, buffers e outros valores usando o algoritmo de clonagem estruturada.
Essa API é especialmente útil com Worker Threads, arquiteturas baseadas em atores, filas internas e componentes que precisam conversar sem compartilhar diretamente todo o estado. Também é possível transferir uma porta para outro worker, criando canais dedicados entre threads.
Neste guia, você aprenderá a criar canais, enviar e receber mensagens, transferir ArrayBuffer e MessagePort, tratar erros, fechar recursos, implementar request-response, aplicar timeouts, controlar filas e evitar vazamentos.
O que é MessageChannel?
MessageChannel cria duas portas ligadas entre si. A documentação oficial de MessageChannel no Node.js descreve a API. O guia de MessageChannel na MDN apresenta o modelo compartilhado com navegadores.
Para trabalho paralelo, consulte Worker Threads no Node.js. Para objetos binários, veja Buffer no Node.js. O artigo sobre Event Loop no Node.js ajuda a entender quando callbacks de mensagem são executados.
Criando um canal
const {
MessageChannel
} = require('node:worker_threads');
const { port1, port2 } = new MessageChannel();Mensagens enviadas por port1 chegam a port2, e vice-versa.
Enviando uma mensagem
port2.on('message', message => {
console.log('Recebido:', message);
});
port1.postMessage({
type: 'greeting',
text: 'Olá'
});Objetos são clonados. O receptor não recebe a mesma referência original.
start()
Ao usar on('message'), a porta normalmente é iniciada automaticamente. Em integrações que usam outras interfaces, start() pode ser necessário:
port2.start();Consulte o comportamento da versão usada.
Recebendo uma única mensagem
port2.once('message', message => {
console.log(message);
});once() evita manter listener quando apenas uma resposta é esperada.
Mensagens suportadas
O mecanismo aceita muitos tipos compatíveis com clonagem estruturada, incluindo:
- objetos e arrays;
- Map e Set;
- Date e RegExp;
- ArrayBuffer e TypedArray;
- erros, conforme versão;
- MessagePort transferível;
- Blob em versões compatíveis.
Funções e alguns objetos nativos não podem ser clonados.
Clonagem e referências
const original = { count: 1 };
port1.postMessage(original);
original.count = 2;O receptor recebe uma cópia lógica com o valor no momento do envio. Não use mensagens como memória compartilhada.
Transferindo ArrayBuffer
const buffer = new ArrayBuffer(1024);
port1.postMessage(
{ buffer },
[buffer]
);Ao incluir o ArrayBuffer na transfer list, a propriedade é movida sem cópia. O buffer original fica destacado e não deve mais ser usado.
Buffer e pool interno
Nem todo Buffer deve ser transferido diretamente. Buffers criados a partir de pools podem compartilhar ArrayBuffer com outros dados. Prefira ArrayBuffer dedicado ou faça uma cópia controlada.
SharedArrayBuffer
SharedArrayBuffer não é transferido; ele permanece compartilhado entre threads. Isso exige sincronização com Atomics e um protocolo rigoroso.
Transferindo uma porta
worker.postMessage(
{ port: port2 },
[port2]
);Após a transferência, a origem não deve continuar usando a porta. O worker pode conversar diretamente com port1.
Canal dedicado entre workers
O processo principal pode criar um MessageChannel e entregar uma extremidade para cada worker. Assim, eles trocam mensagens sem encaminhar tudo pela thread principal.
Request-response
Inclua identificador em cada pedido:
const pending = new Map();
let sequence = 0;
function request(payload) {
const id = ++sequence;
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
port1.postMessage({ id, payload });
});
}Processando respostas
port1.on('message', message => {
const operation = pending.get(message.id);
if (!operation) return;
pending.delete(message.id);
if (message.ok) {
operation.resolve(message.value);
} else {
operation.reject(new Error(message.error));
}
});Valide a estrutura da mensagem antes de acessar propriedades.
Timeout de resposta
function requestWithTimeout(payload, timeout = 5000) {
const id = ++sequence;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pending.delete(id);
reject(new Error('Tempo limite excedido'));
}, timeout);
pending.set(id, {
resolve(value) {
clearTimeout(timer);
resolve(value);
},
reject(error) {
clearTimeout(timer);
reject(error);
}
});
port1.postMessage({ id, payload });
});
}Sem timeout, o Map pode crescer indefinidamente se o receptor falhar.
Cancelamento
Envie uma mensagem de cancelamento com o mesmo ID. O receptor precisa cooperar e interromper o trabalho quando possível.
Veja AbortController no Node.js para sinais e prazos.
Erros de mensagem
port1.on('messageerror', error => {
console.error('Falha ao desserializar', error);
});O evento ajuda a detectar valores que não puderam ser desserializados.
Fechando portas
port1.close();
port2.close();Feche quando o canal não for mais necessário. Remova listeners e rejeite operações pendentes.
Evento close
port1.on('close', () => {
for (const operation of pending.values()) {
operation.reject(new Error('Canal fechado'));
}
pending.clear();
});ref() e unref()
Uma porta ativa pode manter o processo vivo. Em versões compatíveis, use unref() quando o canal não deve impedir o encerramento:
port1.unref();Use ref() para restaurar o comportamento.
Backpressure
postMessage() não fornece o mesmo sinal de backpressure de uma stream. Um produtor rápido pode gerar muitas mensagens e aumentar memória.
Implemente limite de solicitações pendentes, janela de crédito ou confirmações.
Protocolo com créditos
O consumidor informa quantas mensagens pode receber. O produtor envia até o limite e aguarda novos créditos. Esse padrão evita filas ilimitadas.
Tamanho das mensagens
Prefira mensagens pequenas e transfira buffers grandes. Clonar objetos profundos consome CPU e memória.
Validação
function isRequest(message) {
return message
&& Number.isInteger(message.id)
&& typeof message.type === 'string';
}Mesmo entre componentes internos, mensagens podem chegar fora de ordem ou com versão incompatível.
Versionamento do protocolo
{
version: 1,
type: 'resize-image',
id: 42,
payload: {}
}Versionar facilita atualizações graduais de workers.
Segurança
MessageChannel não cria isolamento de segurança por si só. Um worker com acesso ao processo pode consumir CPU, memória e recursos permitidos.
Use o Permission Model no Node.js e controles do sistema como defesa adicional.
Observabilidade
Registre:
- tipo da mensagem;
- ID;
- tempo de resposta;
- fila pendente;
- timeouts;
- erros de serialização;
- canal fechado.
Não registre payloads sensíveis.
Testes
Cubra:
- mensagem simples;
- resposta correlacionada;
- timeout;
- fechamento;
- transferência de ArrayBuffer;
- transferência de porta;
- mensagem inválida;
- cancelamento;
- fila máxima;
- worker encerrado.
Use o Node Test Runner.
Erros comuns
- Não fechar portas: o processo permanece vivo.
- Sem timeout: Promises pendentes acumulam.
- Transferir buffer compartilhado: dados inesperados podem ser afetados.
- Enviar objetos gigantes: clonagem fica cara.
- Sem limite de fila: memória cresce.
- Não validar mensagens: protocolo quebra silenciosamente.
- Usar como sandbox: o canal não limita capacidades.
Boas práticas
- Defina um protocolo versionado.
- Use IDs de correlação.
- Aplique timeouts.
- Limite operações pendentes.
- Transfira buffers grandes.
- Valide mensagens.
- Feche portas.
- Rejeite pedidos no close.
- Monitore filas.
- Teste falhas de worker.
Conclusão
O MessageChannel no Node.js oferece comunicação estruturada entre componentes e Worker Threads por meio de duas MessagePorts conectadas.
O recurso funciona melhor com protocolo explícito, IDs, timeouts, limites e limpeza de recursos. Ao transferir buffers e portas de forma controlada, aplicações conseguem distribuir trabalho entre threads sem depender de estado global compartilhado ou criar filas internas ilimitadas.



