Domine inglês técnico de programação em 2025, seja qual for seu nível. Inscrição gratuita

Node.js: escalabilidade com Clusters e Worker Threads
Rocketseat

Rocketseat

5 min de leitura
node
Imagine como empresas como Netflix e Amazon conseguem atender milhões de requisições simultâneas sem comprometer a performance de suas aplicações. A resposta está na escalabilidade. Quando falamos de Node.js, sua capacidade de lidar com múltiplas requisições é impressionante. Mas, à medida que o tráfego aumenta, o modelo single-threaded do Node.js pode se tornar um gargalo. É aí que entram Clusters e Worker Threads — dois aliados poderosos para criar aplicações robustas e de alta performance.
Neste artigo, você vai aprender como escalar aplicações Node.js de forma prática, utilizando essas duas abordagens, além de entender quando e como aplicá-las no seu projeto.

O desafio da escalabilidade em Node.js

Node.js, apesar de utilizar um modelo single-threaded para processamento de código JavaScript, possui um loop de eventos assíncrono que delega operações de I/O para threads internas, tornando-o eficiente para lidar com APIs e streaming de dados. No entanto, quando o assunto são tarefas intensivas de CPU (como cálculos complexos ou processamento de imagens), esse modelo pode apresentar problemas, impactando diretamente a performance e a experiência do usuário.
Além disso, escalar verticalmente (adicionando mais recursos, como CPU e memória) tem limites, como o custo elevado, consumo de energia, e restrições físicas de hardware que impedem melhorias infinitas. A solução é a escalabilidade horizontal, que utiliza múltiplos processos ou threads para distribuir a carga de trabalho.

Escalando com Clusters no Node.js

O módulo cluster do Node.js permite que você utilize todos os núcleos de CPU disponíveis, criando múltiplos processos (workers) para executar sua aplicação simultaneamente. Cada worker processa as requisições de forma independente, mas compartilha o mesmo servidor.

O que são Clusters?

Os Clusters permitem dividir a carga de trabalho em múltiplos processos, aproveitando assim todos os núcleos do seu processador. Embora todos os workers compartilhem a mesma porta, o processo mestre (master) atua como um "gerente de tráfego", encaminhando as requisições aos workers disponíveis.

Balanceamento de carga e estratégias

Quando falamos em escalar aplicações usando Clusters, o balanceamento de carga é um conceito fundamental. Ele garante que o tráfego de requisições seja distribuído de forma equitativa ou estratégica entre os diversos workers, evitando sobrecarga em um único processo.
Principais estratégias de balanceamento de carga:
  • Round-Robin: atribui requisições sequencialmente a cada worker, de forma cíclica. É simples e funciona bem na maioria dos cenários.
  • IP Hashing: as requisições são encaminhadas ao worker com base no endereço IP do cliente. Essa estratégia garante que o mesmo cliente seja sempre atendido pelo mesmo worker, sendo útil em cenários que exigem afinidade de sessão.
  • Menos Conexões (Least Connections): a requisição é encaminhada ao worker com o menor número de conexões ativas no momento. Esse método busca equilibrar a carga dinamicamente.
Cada estratégia tem suas vantagens e desvantagens. Por exemplo, o round-robin é simples, mas não leva em conta a carga real de cada worker. O IP Hashing garante afinidade, mas pode levar a desequilíbrios se muitos clientes chegarem do mesmo IP. Já a estratégia de menos conexões é mais complexa, mas tende a distribuir a carga de forma mais justa em cenários heterogêneos.

Como funciona?

A mágica acontece graças ao módulo cluster do Node.js. Ele permite que um processo principal (master) gerencie os workers, distribuindo requisições e reiniciando workers em caso de falhas. Por padrão, o Node.js utiliza um método de distribuição interno para balancear requisições entre processos workers. Em implementações modernas, esse método pode se assemelhar ao round-robin, mas detalhes específicos podem variar dependendo do sistema operacional ou da configuração utilizada no ambiente.

Implementando Clusters

const cluster = require("cluster"); const http = require("http"); const os = require("os"); if (cluster.isMaster) { const numCPUs = os.cpus().length; console.log(`Master ${process.pid} is running`); // Cria um worker para cada CPU disponível for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on("exit", (worker) => { console.log(`Worker ${worker.process.pid} died. Reiniciando...`); cluster.fork(); }); } else { http.createServer((req, res) => { res.writeHead(200); res.end(`Handled by worker ${process.pid}`); }).listen(8000); console.log(`Worker ${process.pid} started`); }
Abaixo, um exemplo mais avançado simulando uma operação mais complexa (por exemplo, processamento de imagens ou uma consulta a banco de dados com atraso):
const cluster = require("cluster"); const http = require("http"); const os = require("os"); // Função simulando uma tarefa complexa (ex: processamento de imagem ou query pesada) function heavyTask() { // Simula um atraso de 200ms const start = Date.now(); while (Date.now() - start < 200) { // Loop para simular carga de CPU } return "Tarefa complexa concluída!"; } if (cluster.isMaster) { const numCPUs = os.cpus().length; console.log(`Master ${process.pid} is running`); // Cria um worker por CPU for (let i = 0; i < numCPUs; i++) { cluster.fork(); } // Reinicia o worker em caso de falha cluster.on("exit", (worker) => { console.log(`Worker ${worker.process.pid} died. Reiniciando...`); cluster.fork(); }); } else { // Servidor HTTP que simula uma operação complexa antes de responder http.createServer((req, res) => { const result = heavyTask(); res.writeHead(200, { "Content-Type": "text/plain" }); res.end(`Handled by worker ${process.pid}\n${result}`); }).listen(8000); console.log(`Worker ${process.pid} started`); }
Neste exemplo, cada worker executa a tarefa pesada independentemente, permitindo que a carga seja distribuída entre múltiplos processos. Isso é essencial em cenários de alto tráfego ou processamento complexo.
Caso você queira implementar estratégias de balanceamento específicas, pode usar um proxy reverso como o Nginx ou o HAProxy na frente do seu cluster Node.js, configurando o balanceamento no nível do servidor proxy. Por exemplo, o Nginx permite configurar round-robin, IP Hash, ou até mesmo estratégias mais complexas por meio de módulos.

Limitações

Clusters não resolvem problemas com tarefas extremamente intensivas de CPU, pois cada worker ainda é single-threaded, o que é particularmente relevante para aplicações que exigem cálculos matemáticos complexos ou processamento de imagens. No entanto, permitem paralelizar o processamento de múltiplas requisições, o que pode trazer ganhos de performance em cenários com alta demanda.

O poder do paralelismo com Worker Threads

Enquanto os Clusters criam múltiplos processos, os Worker Threads permitem a execução de código JavaScript em múltiplas threads dentro do mesmo processo. Isso é especialmente útil para offloading de tarefas intensivas de CPU, mantendo o loop de eventos principal responsivo.

Quando usar?

  • Processamento de dados em larga escala.
  • Compressão e descompressão de arquivos.
  • Criptografia.
  • Cálculos matemáticos complexos.
  • Aplicação de filtros em imagens.

Implementando Worker Threads

const { Worker, isMainThread, parentPort, workerData } = require("worker_threads"); if (isMainThread) { const worker = new Worker(__filename, { workerData: 10 }); worker.on("message", (result) => { console.log(`Result from worker: ${result}`); }); worker.on("error", (error) => { console.error(`Worker error: ${error}`); }); worker.on("exit", (code) => { if (code !== 0) { console.error(`Worker stopped with exit code ${code}`); } }); } else { const computeFactorial = (n) => (n <= 1 ? 1 : n * computeFactorial(n - 1)); parentPort.postMessage(computeFactorial(workerData)); }

Implementando Worker Threads

Imagine que você possui um cenário de processamento paralelo: por exemplo, aplicar filtros em um conjunto de imagens ou comprimir múltiplos arquivos ao mesmo tempo.
// workerTask.js const { parentPort, workerData } = require("worker_threads"); const { applyFilterToImage } = require("./imageFilter"); // Função fictícia de filtro // workerData seria um objeto com { imageBuffer: <Buffer da imagem> } const result = applyFilterToImage(workerData.imageBuffer); parentPort.postMessage(result); // main.js const { Worker, isMainThread } = require("worker_threads"); const fs = require("fs"); if (isMainThread) { // Suponha que temos várias imagens para processar const imageFiles = ["img1.jpg", "img2.jpg", "img3.jpg"]; imageFiles.forEach((file) => { const imageBuffer = fs.readFileSync(file); const worker = new Worker("./workerTask.js", { workerData: { imageBuffer }, }); worker.on("message", (filteredImage) => { // Salva a imagem filtrada fs.writeFileSync(`filtered-${file}`, filteredImage); console.log(`Imagem ${file} filtrada com sucesso!`); }); worker.on("error", (err) => { console.error(`Erro no Worker ao processar ${file}:`, err); }); worker.on("exit", (code) => { if (code !== 0) console.error(`Worker finalizado com código de saída ${code}`); }); }); }
Neste exemplo avançado, cada Worker Thread processa uma imagem diferente em paralelo, permitindo que o thread principal continue atendendo requisições ou gerenciando outras tarefas. A ideia pode ser estendida para compressão, criptografia ou análise de dados em tempo real.

Vantagens dos Worker Threads

  • Execução paralela de tarefas intensivas de CPU.
  • Redução de overhead comparado a criar novos processos.
  • Compartilhamento de memória entre threads com SharedArrayBuffer.
⚠️SharedArrayBuffer?
Embora o uso de SharedArrayBuffer permita compartilhar memória entre threads, é importante considerar as implicações de segurança. Por exemplo, ataques do tipo Spectre exploram falhas na execução especulativa para acessar dados sensíveis em memória compartilhada. Para mitigar esse risco, certifique-se de utilizar versões atualizadas do Node.js e habilitar as proteções adequadas no ambiente do sistema operacional.

Desafios

  • Gerenciamento de sincronização entre threads.
  • Evitar condições de corrida e deadlocks.

Clusters vs. Worker Threads: qual escolher?

Característica
Clusters
Worker Threads
Uso ideal
Tarefas de I/O intensivo (ex: requisições HTTP)
Tarefas de CPU intensiva (ex: processamento de imagens)
Estrutura
Múltiplos processos
Múltiplas threads
Compartilhamento
Memória separada
Memória compartilhada
Tolerância a falhas
Alta
Moderada
Vale lembrar que, enquanto os clusters oferecem isolamento de processos, isso vem com o custo de maior overhead de memória. Cada worker é um processo separado, o que significa que consome sua própria parcela de memória. Em contrapartida, Worker Threads, por rodarem dentro do mesmo processo, têm overhead reduzido, mas exigem cuidado extra com sincronização e concorrência.

Combinando Clusters e Worker Threads

Para aplicações de alta performance, você pode combinar as duas abordagens: usar Clusters para distribuir requisições e Worker Threads para lidar com operações intensivas. Aqui está um exemplo:
const cluster = require("cluster"); const os = require("os"); const { Worker } = require("worker_threads"); if (cluster.isMaster) { const numCPUs = os.cpus().length; for (let i = 0; i < numCPUs; i++) { cluster.fork(); } } else { require("./server"); // Código do servidor com Worker Threads }

Conclusão

A escalabilidade é um dos pilares mais importantes para o sucesso de aplicações Node.js, permitindo que elas atendam milhões de requisições de forma eficiente e mantenham uma experiência de usuário de alta qualidade. Ao dominar técnicas como Clusters e Worker Threads, você estará preparado para construir aplicações robustas e prontas para o futuro.
Se você quer levar suas habilidades para o próximo nível e se tornar um especialista em Node.js, a Formação em Node.js da Rocketseat é o caminho ideal! Com mais de 72 horas de conteúdo, 485 aulas gravadas, e 8 projetos profissionais, você aprenderá a criar arquiteturas modernas, escaláveis e alinhadas com as demandas do mercado.
Além disso, a formação oferece:
  • Acompanhamento personalizado por tutores.
  • Certificado validado pelo mercado.
  • Uma comunidade vibrante para networking e troca de experiências.
  • Aulas práticas ministradas por Diego Fernandes, especialista e CTO da Rocketseat.
Transforme seu conhecimento em Node.js em um diferencial competitivo e conquiste as melhores oportunidades no mercado tech.

Aprenda programação do zero e DE GRAÇA

No Discover você vai descomplicar a programação, aprender a criar seu primeiro site com a mão na massa e iniciar sua transição de carreira.

COMECE A ESTUDAR AGORA