Worker Threads: paralelismo real no Node.js

Worker Threads: paralelismo real no Node.js

Robson Rabelo - 12 de julho de 2026 - 1 visualizações

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: ArrayBuffer pode ser transferido (muda de dono, custo zero de cópia).
  • SharedArrayBuffer: memória de fato compartilhada entre threads, com Atomics para 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 vs Worker Threads: qual ferramenta para qual problema

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.
  • maxThreads dimensionado 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.


Leia também

Compartilhar:

Deixe um Comentário

Categorias

Posts Recentes