Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Worker Threads no Node.js

Atualizado em: 28 de setembro de 2026

Estação de trabalho usada no desenvolvimento de aplicações Node.js

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:

  • message para resultado;
  • messageerror para falha de desserialização;
  • error para exceção não tratada;
  • exit para 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

  1. pare de aceitar novas tarefas;
  2. aguarde a fila e tarefas ativas;
  3. aplique prazo máximo;
  4. envie comando de encerramento;
  5. termine threads restantes;
  6. 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.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita