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

Worker Threads no Node.js: Guia Prático

Atualizado em: 27 de julho de 2026

CPU representando Worker Threads e processamento paralelo no Node.js

O Node.js é conhecido por lidar muito bem com operações de entrada e saída, como acessar bancos, consultar APIs e ler arquivos. Porém, tarefas que exigem muito processamento de CPU podem bloquear o event loop e deixar toda a aplicação lenta. Os Worker Threads no Node.js resolvem esse problema permitindo executar código JavaScript em outras threads e aproveitar vários núcleos do processador.

Neste guia, você aprenderá quando usar workers, como enviar dados entre threads, tratar erros, transferir buffers sem cópia, criar um pool de processamento e integrar tarefas pesadas a uma API sem comprometer as demais requisições.

Por que tarefas pesadas bloqueiam o Node.js?

O JavaScript de uma aplicação Node.js normalmente é executado em uma thread principal. O event loop coordena callbacks, Promises, timers e operações assíncronas. Esse modelo funciona muito bem quando o processo passa grande parte do tempo esperando rede, disco ou banco de dados.

O problema aparece quando uma função ocupa a CPU continuamente. Cálculos matemáticos, compressão personalizada, processamento de imagens, geração de relatórios, criptografia intensiva e análise de grandes conjuntos de dados podem impedir que o event loop atenda outras tarefas.

Antes de avançar, revise o que é Node.js, o que é JavaScript e o guia para criar uma API com Node.js.

Exemplo de bloqueio do event loop

Considere uma função que calcula números primos:

function countPrimes(limit) {
  let total = 0;

  for (let number = 2; number <= limit; number++) {
    let prime = true;

    for (let divisor = 2; divisor * divisor <= number; divisor++) {
      if (number % divisor === 0) {
        prime = false;
        break;
      }
    }

    if (prime) total++;
  }

  return total;
}

Se essa função for executada diretamente em uma rota com um limite alto, a thread principal ficará ocupada até o cálculo terminar. Nesse período, outras requisições terão de esperar, mesmo que sejam simples.

Esse comportamento não significa que o Node.js seja lento. Significa apenas que o event loop não deve ser usado para trabalho contínuo de CPU. Para melhorias gerais de endpoints, consulte o artigo sobre performance de APIs Node.js.

O que são Worker Threads?

O módulo nativo node:worker_threads permite criar threads adicionais que executam JavaScript de forma isolada. Cada worker possui seu próprio event loop, contexto global e memória gerenciada pelo motor JavaScript.

O processo principal pode enviar mensagens ao worker e receber resultados. Também é possível transferir determinados buffers ou compartilhar memória explicitamente com SharedArrayBuffer.

A documentação oficial de worker_threads detalha classes, eventos, opções e limitações.

Criando o primeiro worker

Crie um arquivo chamado prime-worker.js:

const { parentPort, workerData } = require('node:worker_threads');

function countPrimes(limit) {
  let total = 0;

  for (let number = 2; number <= limit; number++) {
    let prime = true;

    for (let divisor = 2; divisor * divisor <= number; divisor++) {
      if (number % divisor === 0) {
        prime = false;
        break;
      }
    }

    if (prime) total++;
  }

  return total;
}

const result = countPrimes(workerData.limit);
parentPort.postMessage({ result });

Agora crie main.js:

const path = require('node:path');
const { Worker } = require('node:worker_threads');

const worker = new Worker(
  path.resolve(__dirname, 'prime-worker.js'),
  { workerData: { limit: 5_000_000 } }
);

worker.on('message', message => {
  console.log('Primos encontrados:', message.result);
});

worker.on('error', error => {
  console.error('Falha no worker:', error);
});

worker.on('exit', code => {
  if (code !== 0) {
    console.error('Worker encerrado com código', code);
  }
});

O objeto workerData envia os dados iniciais quando a thread é criada. O método parentPort.postMessage() devolve o resultado à thread principal.

Encapsulando o worker em uma Promise

Em aplicações reais, é mais conveniente integrar workers com async e await:

const path = require('node:path');
const { Worker } = require('node:worker_threads');

function runPrimeWorker(limit) {
  return new Promise((resolve, reject) => {
    const worker = new Worker(
      path.resolve(__dirname, 'prime-worker.js'),
      { workerData: { limit } }
    );

    worker.once('message', message => {
      resolve(message.result);
    });

    worker.once('error', reject);

    worker.once('exit', code => {
      if (code !== 0) {
        reject(new Error(`Worker finalizado com código ${code}`));
      }
    });
  });
}

É importante usar listeners de erro e saída. Um worker pode falhar por exceção, falta de memória, arquivo inexistente ou encerramento forçado.

Usando Worker Threads em uma API Express

Uma rota pode delegar o cálculo para outra thread:

const express = require('express');
const app = express();

app.get('/prime-count', async (req, res, next) => {
  try {
    const limit = Number(req.query.limit);

    if (!Number.isInteger(limit) || limit < 2 || limit > 50_000_000) {
      return res.status(400).json({ error: 'Limite inválido' });
    }

    const result = await runPrimeWorker(limit);
    res.json({ limit, primes: result });
  } catch (error) {
    next(error);
  }
});

Enquanto o worker calcula, a thread principal continua livre para responder a outras rotas. Isso não elimina a necessidade de limites. Um usuário poderia iniciar centenas de tarefas pesadas e esgotar CPU e memória. Valide parâmetros, restrinja concorrência e aplique autenticação quando necessário.

Mensagens entre a thread principal e o worker

workerData é ideal para dados iniciais. Para comunicação contínua, use postMessage() nos dois sentidos:

// main.js
worker.postMessage({
  type: 'process',
  taskId: 'task-42',
  payload: values
});

worker.on('message', message => {
  if (message.type === 'result') {
    console.log(message.taskId, message.payload);
  }
});
// worker.js
const { parentPort } = require('node:worker_threads');

parentPort.on('message', message => {
  if (message.type !== 'process') return;

  const result = processValues(message.payload);

  parentPort.postMessage({
    type: 'result',
    taskId: message.taskId,
    payload: result
  });
});

Defina um formato de mensagens claro. Campos como type, taskId, payload e error ajudam a correlacionar múltiplas tarefas.

O custo da cópia estruturada

Ao enviar objetos com postMessage(), o Node.js usa um algoritmo de clonagem estruturada. Objetos grandes podem consumir tempo e memória porque os dados precisam ser copiados para o outro contexto.

Para cargas pequenas, esse custo costuma ser aceitável. Para buffers grandes, considere transferir a propriedade do ArrayBuffer.

Transferindo buffers sem cópia

const buffer = new ArrayBuffer(1024 * 1024);

worker.postMessage(
  { type: 'process-buffer', buffer },
  [buffer]
);

O segundo argumento é a lista de transferência. Depois do envio, o buffer original deixa de estar disponível na thread principal. Essa abordagem evita uma cópia completa, mas exige cuidado porque a propriedade do bloco de memória muda.

Não transfira um buffer que ainda será usado pelo processo principal. Documente claramente quem é o responsável por cada bloco.

Memória compartilhada com SharedArrayBuffer

Quando duas threads precisam acessar a mesma região de memória, use SharedArrayBuffer e operações atômicas:

const shared = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
const counter = new Int32Array(shared);

const worker = new Worker('./counter-worker.js', {
  workerData: { shared }
});
// counter-worker.js
const { workerData } = require('node:worker_threads');

const counter = new Int32Array(workerData.shared);
Atomics.add(counter, 0, 1);

A memória compartilhada pode melhorar o desempenho, mas aumenta a complexidade. Sem sincronização correta, surgem condições de corrida e resultados imprevisíveis. Prefira troca de mensagens quando a necessidade de compartilhamento não estiver claramente demonstrada.

Por que não criar um worker por requisição?

Criar uma nova thread possui custo. O Node.js precisa inicializar o worker, carregar módulos e criar um novo contexto JavaScript. Em uma rota muito acessada, iniciar um worker para cada requisição pode gerar mais sobrecarga do que benefício.

A solução comum é um pool de workers: um conjunto fixo de threads recebe tarefas de uma fila. Quando um worker termina, ele processa o próximo item.

Definindo o tamanho do pool

O tamanho inicial pode ser baseado em os.availableParallelism(), mantendo pelo menos um núcleo disponível para o processo principal e para o sistema operacional:

const os = require('node:os');

const available = os.availableParallelism();
const poolSize = Math.max(1, available - 1);

A documentação oficial de os.availableParallelism() explica o valor retornado pela API. Esse cálculo é apenas um ponto de partida: o tamanho ideal depende do tipo de tarefa, memória consumida, ambiente de hospedagem e quantidade de processos da aplicação.

Estrutura de um pool

Um pool normalmente mantém:

  • uma lista de workers disponíveis;
  • uma fila de tarefas pendentes;
  • um identificador para cada tarefa;
  • Promises para resolver ou rejeitar resultados;
  • tratamento de falhas e substituição de workers;
  • limite máximo para a fila.

Fila com limite

class TaskQueue {
  constructor(maxPending = 100) {
    this.maxPending = maxPending;
    this.items = [];
  }

  push(task) {
    if (this.items.length >= this.maxPending) {
      throw new Error('Fila de processamento cheia');
    }

    this.items.push(task);
  }

  shift() {
    return this.items.shift();
  }
}

Quando a fila atinge o limite, a aplicação deve rejeitar ou adiar novas solicitações. Retornar um status como 429 ou 503 pode ser mais seguro do que aceitar trabalho ilimitado.

Timeout e cancelamento

Uma tarefa pode entrar em loop, demorar além do aceitável ou consumir recursos excessivos. Implemente timeout e encerre o worker quando necessário:

function runWithTimeout(worker, timeoutMs) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(async () => {
      await worker.terminate();
      reject(new Error('Tempo limite excedido'));
    }, timeoutMs);

    worker.once('message', message => {
      clearTimeout(timer);
      resolve(message);
    });

    worker.once('error', error => {
      clearTimeout(timer);
      reject(error);
    });
  });
}

terminate() interrompe a thread inteira. Em um pool, talvez seja necessário criar um novo worker para substituir o encerrado. Para cancelamento cooperativo, envie uma mensagem e faça o worker verificar periodicamente se deve parar.

Limites de recursos

O construtor Worker aceita a opção resourceLimits, que permite restringir alguns limites do motor JavaScript:

const worker = new Worker('./worker.js', {
  workerData,
  resourceLimits: {
    maxOldGenerationSizeMb: 256,
    maxYoungGenerationSizeMb: 64,
    stackSizeMb: 4
  }
});

Esses limites ajudam a reduzir o impacto de tarefas inesperadas, mas não substituem validação, filas, monitoramento e isolamento de infraestrutura.

Worker Threads, child_process ou cluster?

As três opções resolvem problemas diferentes:

  • Worker Threads: paralelismo de CPU dentro do mesmo processo, com comunicação eficiente e possibilidade de memória compartilhada.
  • child_process: execução de outro processo, comando ou programa, com isolamento maior e comunicação por canais do sistema operacional.
  • cluster: múltiplos processos Node.js atendendo conexões, útil para distribuir servidores HTTP entre núcleos.

Use workers para tarefas computacionais. Use processos filhos para executar ferramentas externas ou quando o isolamento de processo for desejável. Use múltiplas instâncias da aplicação para aumentar a capacidade de atender requisições.

Em ambientes com contêineres, o escalonamento também pode acontecer fora do Node.js. O guia sobre Docker ajuda a entender esse modelo.

Quando Worker Threads não ajudam?

Workers não tornam operações de rede mais rápidas. Consultas HTTP, leitura de banco e espera por arquivos já são tratadas de maneira assíncrona pelas APIs do Node.js. Colocar essas operações em outra thread pode apenas adicionar complexidade e custo de comunicação.

Também não vale a pena usar workers para funções muito curtas. A inicialização, serialização e coordenação podem custar mais do que executar o código diretamente.

Os melhores candidatos são tarefas mensuravelmente pesadas, independentes e com entrada e saída relativamente pequenas.

Dividindo um trabalho grande

Uma tarefa pode ser dividida em partes e distribuída para vários workers. Por exemplo, um conjunto grande de números pode ser separado em blocos. Cada thread processa um intervalo e o processo principal combina os resultados.

Dividir demais gera sobrecarga de mensagens. Dividir de menos pode deixar alguns workers ociosos enquanto outro executa um bloco muito maior. Teste diferentes tamanhos com dados reais.

Tratamento de erros

Dentro do worker, capture erros esperados e devolva um formato previsível:

parentPort.on('message', message => {
  try {
    const result = processTask(message.payload);

    parentPort.postMessage({
      taskId: message.taskId,
      ok: true,
      result
    });
  } catch (error) {
    parentPort.postMessage({
      taskId: message.taskId,
      ok: false,
      error: {
        name: error.name,
        message: error.message
      }
    });
  }
});

Não envie stack traces, caminhos internos ou dados sensíveis ao cliente final. Registre detalhes no servidor e devolva uma mensagem segura.

Observabilidade e métricas

Monitore quantidade de workers ativos, tamanho da fila, tempo de espera, duração das tarefas, taxa de erro, timeouts, memória, CPU e quantidade de threads reiniciadas.

Essas métricas mostram se o pool está pequeno, se a fila cresce continuamente ou se uma tarefa específica está consumindo recursos demais.

Testando workers

Mantenha a lógica de negócio separada da infraestrutura do worker. A função de cálculo pode ser testada diretamente, enquanto testes de integração verificam mensagens, erros e encerramento.

Inclua casos com entrada inválida, tarefa que lança exceção, timeout, worker encerrado e fila cheia. Teste também múltiplas tarefas simultâneas para identificar problemas de correlação.

Não dependa apenas de testes unitários. Um pool pode funcionar com uma tarefa e falhar sob concorrência, especialmente quando memória e CPU estão próximas do limite.

Worker Threads e respostas em tempo real

Uma tarefa longa nem sempre deve manter uma requisição HTTP aberta. A API pode criar o trabalho, devolver um identificador e permitir que o cliente acompanhe o status. Para comunicação bidirecional, veja o artigo sobre WebSocket com Node.js.

Esse modelo desacopla o tempo de processamento do tempo limite da requisição e facilita retentativas, histórico e escalabilidade.

Boas práticas para produção

  • Use workers apenas para tarefas de CPU comprovadamente pesadas.
  • Prefira um pool em vez de criar uma thread por requisição.
  • Limite o tamanho da fila e a concorrência.
  • Valide o tamanho e o formato da entrada.
  • Implemente timeout e cancelamento.
  • Trate os eventos message, error e exit.
  • Substitua workers que falharam em um pool.
  • Evite copiar objetos enormes entre threads.
  • Use memória compartilhada somente quando necessário.
  • Monitore CPU, memória, fila e duração.
  • Faça testes de carga no mesmo tipo de ambiente da produção.
  • Não exponha mensagens internas de erro ao usuário.

Conclusão

Worker Threads no Node.js são uma ferramenta importante para aproveitar múltiplos núcleos sem bloquear o event loop. Elas são especialmente úteis em cálculos, transformações, compressão, análise de dados e outras tarefas intensivas de CPU.

Comece isolando a função pesada em um worker e medindo o resultado. Em seguida, adicione tratamento de erros, timeout e limites. Quando o volume crescer, use um pool com fila controlada e métricas. Com essa arquitetura, a aplicação continua responsiva mesmo enquanto executa trabalhos computacionais exigentes.

Os 10 Melhores Cursos de Programação de 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