Worker Threads permitem executar JavaScript em threads adicionais dentro do mesmo processo Node.js. Eles são indicados para tarefas intensivas de CPU, como processamento de imagens, compressão, criptografia, parsing pesado, cálculos e transformação de grandes conjuntos de dados.
O event loop principal é excelente para I/O concorrente, mas uma função síncrona longa bloqueia todas as requisições atendidas por aquela thread. Um worker move esse trabalho para outro isolate do V8 e mantém a aplicação responsiva.
Quando usar
Worker Threads ajudam quando a tarefa:
- consome CPU por dezenas ou centenas de milissegundos;
- pode ser representada como entrada e saída;
- não depende continuamente do estado da thread principal;
- é executada várias vezes;
- bloqueia o event loop durante carga;
- pode aproveitar vários núcleos.
Para banco de dados, HTTP e arquivos assíncronos, workers normalmente não melhoram o resultado. Essas operações já usam mecanismos não bloqueantes ou o thread pool interno.
Primeiro worker
Arquivo worker.js:
import { parentPort, workerData } from 'node:worker_threads';
function calculate(limit) {
let total = 0;
for (let i = 0; i < limit; i += 1) {
total += Math.sqrt(i);
}
return total;
}
const result = calculate(workerData.limit);
parentPort.postMessage({ result });Thread principal:
import { Worker } from 'node:worker_threads';
function runCalculation(limit) {
return new Promise((resolve, reject) => {
const worker = new Worker(
new URL('./worker.js', import.meta.url),
{ workerData: { limit } },
);
worker.once('message', resolve);
worker.once('error', reject);
worker.once('exit', (code) => {
if (code !== 0) reject(new Error(`Worker encerrou com código ${code}`));
});
});
}
const result = await runCalculation(100_000_000);
console.log(result);Use new URL(..., import.meta.url) em ES Modules para resolver o arquivo de forma previsível.
Mensagens e clonagem
postMessage usa o algoritmo de clonagem estruturada. Objetos, arrays, mapas, sets e buffers compatíveis podem ser enviados, mas funções e muitos objetos de biblioteca não podem.
Clonar entradas grandes custa CPU e memória. Reduza o payload ou use transfer list.
Transferindo ArrayBuffer
const buffer = new ArrayBuffer(1024 * 1024);
worker.postMessage({ buffer }, [buffer]);Ao transferir, a thread remetente perde acesso ao conteúdo transferido. Isso evita cópia, mas exige desenho cuidadoso de propriedade.
SharedArrayBuffer
SharedArrayBuffer permite memória compartilhada. As threads podem acessar os mesmos bytes e coordenar por Atomics.
const shared = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
const state = new Int32Array(shared);
worker.postMessage({ shared });
Atomics.store(state, 0, 1);
Atomics.notify(state, 0);Memória compartilhada introduz condições de corrida e deadlocks. Prefira troca de mensagens até que o custo de cópia seja comprovadamente relevante.
Não crie um worker por requisição
Inicializar uma thread carrega um novo isolate, módulos e memória. Criar e destruir um worker para cada chamada pode custar mais que a tarefa. Use um pool fixo.
Pool de workers
Um pool mantém threads prontas e distribui tarefas:
import { Worker } from 'node:worker_threads';
import os from 'node:os';
const size = Math.max(1, os.availableParallelism() - 1);
const workers = Array.from({ length: size }, () => ({
worker: new Worker(new URL('./pool-worker.js', import.meta.url)),
busy: false,
}));Uma implementação completa precisa de fila, IDs de tarefa, timeouts, tratamento de falha, substituição de worker e encerramento. Bibliotecas de pool podem reduzir erros.
Worker que processa várias tarefas
import { parentPort } from 'node:worker_threads';
parentPort.on('message', ({ id, payload }) => {
try {
const result = processPayload(payload);
parentPort.postMessage({ id, result });
} catch (error) {
parentPort.postMessage({
id,
error: { name: error.name, message: error.message },
});
}
});Não envie objetos Error diretamente esperando preservar toda a estrutura. Serialize somente campos seguros.
Timeout e cancelamento
function executeWithTimeout(worker, task, timeoutMs = 5000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error('Tempo limite do worker'));
}, timeoutMs);
function onMessage(message) {
if (message.id !== task.id) return;
clearTimeout(timer);
worker.off('message', onMessage);
resolve(message.result);
}
worker.on('message', onMessage);
worker.postMessage(task);
});
}Um timeout na Promise não interrompe automaticamente o cálculo. Para cancelamento cooperativo, compartilhe um sinal ou divida o trabalho em etapas. worker.terminate() encerra a thread inteira e pode descartar outras tarefas.
AbortController
Um AbortSignal não é necessariamente transferido como objeto funcional. Envie mensagens de cancelamento ou use um flag em SharedArrayBuffer.
Erros e exits
Trate os eventos:
messagepara resultado;messageerrorpara falha de desserialização;errorpara exceção não tratada;exitpara encerramento.
Se um worker do pool falhar, remova-o, rejeite a tarefa ativa e crie uma substituição com backoff. Falhas repetidas podem indicar entrada inválida ou bug determinístico; não reinicie infinitamente sem limite.
Limites de recurso
O construtor aceita opções para limitar recursos do isolate. Isso ajuda a controlar memória, mas limites muito baixos causam encerramentos.
new Worker(url, {
resourceLimits: {
maxOldGenerationSizeMb: 256,
stackSizeMb: 4,
},
});Monitore falhas e ajuste com carga real.
Contexto e observabilidade
AsyncLocalStorage não atravessa automaticamente a thread. Envie request ID e trace context:
worker.postMessage({
id: taskId,
payload,
context: {
requestId: requestContext.getStore()?.requestId,
},
});No worker, crie um contexto local para a execução da tarefa. Não envie o store inteiro.
Logs
Inclua threadId, taskId e versão:
import { threadId } from 'node:worker_threads';
logger.info({ threadId, taskId }, 'Tarefa iniciada');Evite gerar um log por item em loops grandes. Agregue progresso.
Métricas
Monitore:
- tamanho da fila;
- workers ocupados;
- tempo de espera;
- tempo de processamento;
- timeouts;
- falhas e reinicializações;
- CPU por processo;
- memória;
- event loop utilization de cada worker.
Uma fila crescente indica que a chegada supera a capacidade. Aumentar threads acima do número de CPUs pode piorar por contenção.
Quantidade de workers
os.availableParallelism() é um bom ponto de partida, mas deixe capacidade para a thread principal e outros processos. Em containers, confirme se o runtime reconhece os limites de CPU.
Teste tamanhos diferentes. Tarefas com muita memória podem exigir menos workers.
Worker Threads e cluster
Cluster cria processos que atendem conexões; Worker Threads executam CPU em paralelo dentro de um processo. Uma arquitetura pode usar múltiplos pods, um processo por pod e um pequeno pool de workers.
Multiplicar processos e workers sem cálculo pode criar dezenas de threads por máquina e causar throttling.
ESM e TypeScript
O worker precisa executar JavaScript compatível com o ambiente. Em produção, compile o arquivo do worker. Ferramentas de desenvolvimento como loaders TypeScript podem adicionar custo e diferenças.
new Worker(new URL('./worker.js', import.meta.url), {
type: 'module',
});Graceful shutdown
- pare de aceitar novas tarefas;
- aguarde a fila e tarefas ativas;
- aplique prazo máximo;
- envie comando de encerramento;
- termine threads restantes;
- encerre o processo.
Defina o que acontece com tarefas interrompidas. Em sistemas críticos, persista a fila externamente.
Teste de desempenho
Compare três cenários: execução na thread principal, um worker novo por tarefa e pool. Meça throughput, p99, CPU, memória, tempo de fila e custo de serialização.
Uma tarefa pequena pode ficar mais lenta no worker devido a mensagens. Agrupe itens em lotes quando adequado.
Erros comuns
- usar workers para I/O comum;
- criar uma thread por requisição;
- enviar payloads enormes;
- ignorar custo de clonagem;
- usar workers demais;
- não tratar exit e error;
- não limitar fila;
- não implementar shutdown;
- assumir propagação automática de contexto;
- compartilhar memória sem sincronização.
Fluxo recomendado
Primeiro confirme o bloqueio com Event Loop Utilization e CPU Profiling. Depois isole a função CPU-bound, crie um pool pequeno, limite a fila e meça o custo de mensagens. Use AsyncLocalStorage para contexto na thread principal e propague apenas IDs necessários.
Consulte a documentação oficial de Worker Threads e a referência oficial de availableParallelism.

