Worker Threads: paralelismo real no Node.js
Node.js tem threads de verdade desde a v12. Quando usar worker threads (CPU pesada), por que pool é obrigatório, o custo escondido da passagem de dados e a tabela mental cluster × threads × fila.
"Node.js é single-threaded" é uma daquelas verdades que envelheceram: desde a versão 12, a plataforma tem threads de verdade no módulo worker_threads. A questão deixou de ser "dá para paralelizar?" e virou "quando vale a pena, e como fazer sem se machucar?".
O problema que Worker Threads resolve (e só ele)
Recapitulando a série: o event loop faz I/O voar numa thread só, e o cluster multiplica processos para usar todos os núcleos. Sobra um vilão: a tarefa de CPU pesada no meio da requisição: gerar um PDF grande, redimensionar imagem, comprimir dados, calcular um relatório, validar uma planilha de 200 mil linhas.
Cluster não resolve isso (o worker do cluster que pegar a tarefa trava do mesmo jeito para os usuários dele). Worker Threads resolve: a conta pesada roda numa thread paralela, e o event loop continua livre para atender.
// hash-worker.js: roda em thread separada
const { parentPort, workerData } = require("node:worker_threads");
const { pbkdf2Sync } = require("node:crypto");
const hash = pbkdf2Sync(workerData.senha, workerData.salt, 310_000, 64, "sha512");
parentPort.postMessage(hash.toString("hex"));
// no endpoint: dispara e segue atendendo
const { Worker } = require("node:worker_threads");
function hashSenha(senha, salt) {
return new Promise((resolve, reject) => {
const w = new Worker("./hash-worker.js", { workerData: { senha, salt } });
w.once("message", resolve);
w.once("error", reject);
});
}
app.post("/cadastro", async (req, res) => {
const hash = await hashSenha(req.body.senha, gerarSalt());
// ... o event loop ficou LIVRE durante o cálculo
res.status(201).json({ ok: true });
});
Cada worker tem seu próprio event loop e sua própria instância do V8, mas vive dentro do mesmo processo: mais leve que um fork, com comunicação por mensagens (documentação oficial).
A regra de ouro: use um pool
O exemplo acima tem um defeito proposital: criar um worker por chamada. Subir uma thread custa dezenas de milissegundos e memória; sob carga, isso vira o novo gargalo. Produção usa pool: N workers criados uma vez, reutilizados para cada tarefa.
Dá para escrever o seu, mas não precisa: a biblioteca Piscina, mantida por gente do core do Node, resolve fila, reuso e limites (github.com/piscinajs/piscina):
const Piscina = require("piscina");
const pool = new Piscina({
filename: "./tarefa-pesada.js",
maxThreads: 4, // deixe núcleos livres para o event loop
});
app.post("/relatorio", async (req, res) => {
const resultado = await pool.run(req.body); // enfileira e executa no pool
res.json(resultado);
});
Dimensionamento: workers de CPU disputam núcleo com o event loop e com o cluster. A conta prática num servidor de 8 núcleos rodando cluster: menos processos de cluster (por exemplo 4) + pool pequeno por processo, ou um serviço dedicado só para o trabalho pesado.
Passagem de dados: o custo escondido
Mensagens entre threads são copiadas (algoritmo de clone estruturado). Mandar 200MB de dados para o worker copia 200MB, e a cópia acontece... no event loop. As saídas:
- Transferir em vez de copiar:
ArrayBufferpode ser transferido (muda de dono, custo zero de cópia). SharedArrayBuffer: memória de fato compartilhada entre threads, comAtomicspara coordenação. Poderoso e fácil de errar; use para os casos extremos.- Repensar a fronteira: muitas vezes o worker deve ler a fonte diretamente (arquivo, banco) em vez de receber os dados mastigados.

Cluster ou Worker Threads? (a tabela mental)
- Escalar requisições de I/O pela máquina: cluster (processos, isolamento forte).
- Tarefas de CPU dentro da aplicação: worker threads com pool (threads, comunicação barata).
- Trabalho pesado, frequente e demorado (minutos): nenhum dos dois; fila + serviço worker separado, que escala e falha de forma independente.
- E combinações são normais: cluster para as requisições, um pool de threads pequeno para o pico de CPU ocasional.
O erro a evitar: usar worker threads para I/O ("vou paralelizar minhas chamadas de banco!"). I/O já é paralelo no event loop de graça; threads aí só adicionam overhead.
Checklist de produção
- Pool sempre; worker por requisição, nunca.
maxThreadsdimensionado deixando núcleo para o event loop.- Timeout e tratamento de erro por tarefa (worker que trava não pode segurar a fila).
- Event loop lag monitorado antes e depois: é a métrica que prova que a mudança valeu.
Dimensionar essa combinação (cluster × threads × filas) para a carga real de cada aplicação é trabalho recorrente no nosso serviço de Desenvolvimento Web, Mobile e Desktop.
Qual endpoint da sua API faz conta pesada no caminho da requisição? Conte para a IA da Rabelo Digital, aqui no canto da tela, e ela indica se o caso pede pool de threads ou fila. Sem formulário, sem espera.


Deixe um Comentário