Node.js com múltiplos CPUs: cluster e worker_threads
Node.js é single-threaded por design — um processo usa apenas 1 vCPU. Em VPS com 2 ou 4 vCPUs, você paga por recursos que a aplicação não usa. Módulo cluster cria múltiplos processos worker para apps HTTP; worker_threads paraleliza operações CPU-intensivas dentro do mesmo processo. Saber qual usar dobra ou quadruplica o throughput sem trocar de servidor.
Módulo cluster: múltiplos processos HTTP
O módulo cluster do Node.js cria um processo master que distribui conexões TCP entre workers — cada worker é um processo Node.js completo rodando em seu próprio vCPU:
// src/cluster.ts
import cluster from 'cluster'
import { availableParallelism } from 'os'
import process from 'process'
const NUM_WORKERS = availableParallelism() // número de vCPUs
if (cluster.isPrimary) {
console.log(`Master ${process.pid} rodando. Iniciando ${NUM_WORKERS} workers...`)
// Criar um worker por vCPU
for (let i = 0; i < NUM_WORKERS; i++) {
cluster.fork()
}
// Reiniciar worker morto automaticamente
cluster.on('exit', (worker, code, signal) => {
console.warn(`Worker ${worker.process.pid} morreu (code:${code}). Reiniciando...`)
cluster.fork()
})
} else {
// Este código roda em cada worker
import('./server').then(({ startServer }) => {
startServer()
console.log(`Worker ${process.pid} rodando`)
})
}worker_threads: paralelismo CPU-bound
Worker threads rodam em threads do mesmo processo, compartilhando memória via SharedArrayBuffer — ideal para cálculos pesados que travariam o event loop:
// src/workers/hash-worker.ts
import { workerData, parentPort } from 'worker_threads'
import crypto from 'crypto'
// Esta função roda em uma thread separada
function hashIntensivo(dados: string): string {
let resultado = dados
for (let i = 0; i < 100000; i++) {
resultado = crypto.createHash('sha256').update(resultado).digest('hex')
}
return resultado
}
parentPort?.postMessage(hashIntensivo(workerData.input))
// src/services/hash.service.ts
import { Worker } from 'worker_threads'
import path from 'path'
export function hashComWorker(input: string): Promise<string> {
return new Promise((resolve, reject) => {
const worker = new Worker(
path.join(__dirname, 'workers/hash-worker.js'),
{ workerData: { input } }
)
worker.on('message', resolve)
worker.on('error', reject)
worker.on('exit', (code) => {
if (code !== 0) reject(new Error(`Worker encerrou com código ${code}`))
})
})
}
// Uso em rota Express (não bloqueia o event loop)
app.post('/hash', async (req, res) => {
const resultado = await hashComWorker(req.body.input)
res.json({ hash: resultado })
})Pool de worker_threads: reutilizar threads
Criar uma nova thread por requisição é caro. Um pool reutiliza threads para múltiplas tarefas:
// npm install piscina (pool de worker_threads)
import Piscina from 'piscina'
import path from 'path'
const pool = new Piscina({
filename: path.join(__dirname, 'workers/processamento.js'),
minThreads: 2,
maxThreads: 4, // máximo de threads no pool
idleTimeout: 30000, // remover threads ociosas após 30s
})
// Usar o pool (espera uma thread disponível)
app.post('/processar', async (req, res) => {
try {
const resultado = await pool.run({ dados: req.body.dados })
res.json(resultado)
} catch (err) {
res.status(500).json({ error: err.message })
}
})
// Monitorar o pool
console.log('Threads ativas:', pool.threads.length)
console.log('Tarefas na fila:', pool.queueSize)Quando usar cluster vs worker_threads
A escolha depende do tipo de operação que você quer paralelizar:
# cluster mode — use para:
# ✓ APIs HTTP que recebem muitas requisições simultâneas (I/O-bound)
# ✓ Cada worker processa requisições independentes
# ✓ Isolamento: se um worker travar, os outros continuam
# ✓ Compatível com PM2 --cluster
# worker_threads — use para:
# ✓ Operações CPU-intensivas dentro de uma requisição
# (resize de imagem, criptografia, parsing de arquivo grande)
# ✓ Cálculos numéricos (ML inference, compressão)
# ✓ Processamento paralelo de arrays grandes
# ✓ Compartilhamento de memória via SharedArrayBuffer
# Regra geral:
# "Muitas requisições pequenas" → cluster (PM2)
# "Uma requisição com processamento pesado" → worker_threads
# Métricas para decidir:
# CPU do processo Node.js > 80%? → worker_threads para offload
# Latência alta mesmo com CPU baixo? → I/O wait, não precisa de parallelismDetectar bloqueio do event loop
Event loop bloqueado é o maior problema de performance em Node.js. Detecte com métricas de lag:
// Medir lag do event loop (operação que NÃO deve demorar)
function medirEventLoopLag(): Promise<number> {
return new Promise((resolve) => {
const inicio = Date.now()
setImmediate(() => {
resolve(Date.now() - inicio) // deve ser < 5ms em condições normais
})
})
}
// Monitorar continuamente
setInterval(async () => {
const lag = await medirEventLoopLag()
if (lag > 100) {
logger.warn('Event loop lag alto', { lag_ms: lag })
// Alerta: CPU-bound bloqueando o loop
}
}, 5000)
// Alternativa: usar clinic.js para diagnóstico detalhado
// npm install -g clinic
// clinic doctor -- node dist/index.js
// Gera relatório HTML com gargalos identificados$ runstack deploy --plan starter
Não quer configurar manualmente?
Não quer configurar manualmente? Implante o VPS em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.