Node.js 10 min de leitura 27 fev 2026

Node.js mutilthreading? Sim é possível, conheça os Worker Threads

Fala Dev! Tudo em riba?

Você já teve aquela sensação de frustração quando sua aplicação Node.js simplesmente “trava” durante uma tarefa pesada? É famoso Event Loop bloqueado. Fazendo uma analogia, imagine que você é o único garçom em um restaurante movimentado, consegue atender bem quando são pedidos simples, mas quando um cliente pede aquele prato mega elaborado, todos os demais ficam esperando pois o preparo do prato desse cliente demora um pouco mais que o preparo dos pratos dos demais clientes.

É exatamente isso que acontece com o Node.js, ele é fenomenal para operações de I/O (leitura de arquivos, requisições de rede), mas quando encontra tarefas que demandam muito processamento da CPU, o Event Loop fica bloqueado e sua aplicação fica “congelada”.

No entanto, é possível evitar esse tipo de problema, direcionando o preparo desse prato elaborado para um “chefe”, um “trabalhador” que tem como função executar o preparo de tais tipos de pratos e liberar a thread principal, ou o “chefe” principal, para continuar executando os demais preparos. Para isso entenderemos o conceito de Worker Threads.

Mas Afinal o Que São os Worker Threads?

Os Worker Threads foram introduzidos no Node.js versão 10.5.0 como uma feature experimental e se tornaram estáveis na versão 12. É um módulo nativo que permite gerar vários threads de execução em um único processo.

Por baixo dos pano, cada Worker Thread criado é executado em seu próprio ambiente V8 isolado, evitando bloqueios no Event Loop que gerencia a thread principal. Apesar de serem executado em ambientes isolados, os Worker Threads podem compartilhar recursos com a thread principal como memória, por exemplo.

Quando Utilizar os Worker Threads?

Antes de mergulharmos nos Worker Threads, é crucial entender quando eles são realmente necessários. Nem toda tarefa precisa de uma thread separada, apenas tarefas que demando uso intenso de CPU. Vejamos algumas tarefas que demandando um uso maior de CPU e outras que não demandando tanta CPU:

Resumidamente, se uma tarefa faz a CPU “suar”, use Worker Threads. Se ela apenas espera por dados externos, o Node.js assíncrono já resolve.

Worker Threads vs. Outras Alternativas

Você pode estar se perguntando: “Por que não usar child_process ou cluster?” Ótima pergunta! Vamos esclarecer citando características de cada um:

Mão na Massa: Consumindo endpoints de uma API sem utilizar Worker Thread

Suponhamos que você tenha uma API com os seguintes endpoints:

A ideia aqui é demonstrar que, ao requisitar o endpoint de calculo do Fibonacci e de health ao mesmo tempo, notaremos que enquanto o processamento do endpoint de Fibonacci não terminar, a requisição do endpoint de health, cujo o código a ser processado é muito simples, vai ficar aguardando até o calculo do Fibonacci ser concluído.

Vamos preparar o projeto para os testes. Em uma pasta em seu ambiente de desenvolvimento, crie a seguinte estrutura de pastas e arquivos:

Caso queira adiantar essa etapa, sugiro acessar meu repositório no Github e clonar essa estrutura.

Vamos instalar as dependências necessárias e crie o script para iniciar a aplicação conforme o package.json abaixo:

{
  "name": "worker-threads",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "dev": "node --experimental-strip-types --no-warnings --watch src/server.ts"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "fastify": "^5.5.0"
  },
  "devDependencies": {
    "@types/node": "^24.3.0",
    "autocannon": "^8.0.0"
  }
}

Note que uma das dependências é o Autocannon. Nós utilizaremos ele para simular um teste de carga no qual serão feitas várias requisições simultâneas junto ao endpoint de calculo do Fibonacci para demonstrar o Event Loop bloqueado pelo cálculo. Para esse teste, o arquivo script.js deve conter o seguinte código:

import autocannon from 'autocannon'

autocannon({
  url: 'http://localhost:3333/fibonacci/40',
  connections: 5, 
  pipelining: 1, 
  duration: 20 
}, console.log);

E no arquivo server.ts, teremos o seguinte código:

E no arquivo server.ts, teremos o seguinte código:

import Fastify from 'fastify';

function fibonacci(n:number): number {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

const fastify = Fastify({
  logger: false
})

fastify.get('/health', async function (_request, reply) {
  reply.send({status: 'ok'})
})

fastify.get<{ Params: { number?: number } }>('/fibonacci/:number?', async function (request, reply) {
  const { number } = request.params;
  const result =  fibonacci(number || 10);
  console.log('Fibonacci result:', result);
  
  reply.send({result})
})

fastify.listen({ port: 3333 })

Nele, basicamente temos:

Vamos iniciar o serviço da nossa API através do npm.

npm run dev

Agora vamos executar o script de teste de carga que irá enviar requisições para o endpoint GET /fibonacci/:number e simultaneamente vamos tentar executar o endpoint GET /health.

Para executar o script de teste de carga, utilizaremos o seguinte comando:

node autocannon/script.js

Uma observação: o script de teste de carga acima está configurado para enviar até 5 requisições simultâneas durante 20 segundos.

A imagem abaixo ilustra a execução do script de teste de carga juntamente com uma requisição junto ao endpoint GET /health.

Podemos notar a resposta da requisição ao endpoint GET /health demorou 21.9 segundos para ser retornada.

Analisando o relatório gerado pelo Autocannon, notamos que foram enviadas 17 requisições ao endpoint GET /fibonacci/:number durante os 20 segundos de teste e apenas 6 retornaram sucesso dentro dos 20 segundos.

Sendo assim, foi possível notar que o processamento necessário para atender as requisições de calculo do Fibonacci impactou diretamente o tempo de retorno da requisição do endpoint de GET /health.

Adicionando o Worker Thread na brincadeira

Vamos agora melhorar nossa API delegando o processamento pesado para um worker. Se você lembra da imagem da estrutura de arquivos do projeto, nele continha um arquivo fibonacci-worker.ts. O conteúdo dele é o seguinte:

import { parentPort, workerData } from "worker_threads";

function fibonacci(n: number): number {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

const result = fibonacci(workerData);
parentPort!.postMessage(result);

Explicando linha a linha:

Agora vamos precisar modificar um pouco nosso arquivo server.ts.

import Fastify from 'fastify';
import { Worker } from "worker_threads";

function runFibonacci(n: number): Promise<number> {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./src/fibonacci-worker.ts', {
      workerData: n,
    });

    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0)
        reject(new Error(`Worker stopped with exit code ${code}`));
    });
  });
}

const fastify = Fastify({
  logger: false
})

fastify.get('/health', async function (_request, reply) {
  reply.send({status: 'ok'})
})


fastify.get<{ Params: { number?: number } }>('/fibonacci/:number?', async function (request, reply) {
  const { number } = request.params;
  const result = await runFibonacci(number || 10);
  
  console.log('Fibonacci result:', result);
  
  reply.send({result})
})

fastify.listen({ port: 3333 })

Vamos entender o que foi modificado:

const worker = new Worker('./src/fibonacci-worker.ts', {
  workerData: n,
});
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
  if (code !== 0)
    reject(new Error(`Worker stopped with exit code ${code}`));
});

Agora vamos executar novamente nosso servidor, executar o script de teste de carga e requisitar o endpoint GET /health simultaneamente.

Na imagem acima podemos notar uma melhora impressionante no tempo de resposta do endpoint GET /health, cujo o tempo de 21.9 segundos passou para 11 milissegundos. Isso mesmo, 11 milissegundos!

Analisando o relatório gerado pelo Autocannon, podemos notar também uma melhora incrível. No relatório anterior foi possível enviar 17 requisições das quais apenas 5 responderam dentro do tempo de 20 segundos do teste. Com o worker, foi possível enviar 35 requisições das quais 30 responderam dentro do período de teste. Isso mesmo, delegando o processamento para um worker, foi possível atender os dois endpoints com excelente performance.

Mais alguns motivos para usar os Worker Threads

Outros motivos que podem contribuir na decisão da utilização dos Worker Threads:

Considerações Finais

Worker Threads são uma ferramenta poderosa que pode transformar drasticamente a performance de aplicações Node.js que lidam com tarefas de uso intensivo de CPU. Mas lembre-se: com grandes poderes, vêm grandes responsabilidades.

Use Worker Threads quando:

Evite Worker Threads quando:

A chave é sempre medir primeiro, otimizar depois. Use ferramentas de profiling para identificar gargalos reais antes de partir para Worker Threads. E quando usar, faça-o de forma responsável, gerencie workers adequadamente, trate erros, monitore performance e sempre limpe recursos.

Com esses conhecimentos, você está pronto para levar suas aplicações Node.js para o próximo nível de performance. Agora é só colocar a mão na massa e fazer esses workers trabalharem para você!

Se curtiu o assunto, no vídeo abaixo eu mostro em detalhes como utilizar os Worker Threads, trago alguns conceitos e executo alguns testes 👇

Fico por aqui e até a próxima!

#event loop #multi thread #nodejs #perofrmance #thread