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:

    typescript
    // 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`)
      })
    }
    Dica
    availableParallelism() (Node.js 19+) retorna o número de vCPUs lógicos disponíveis. Em versões anteriores: os.cpus().length. PM2 em cluster mode faz o mesmo automaticamente — use cluster nativo apenas se quiser controle direto.

    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:

    typescript
    // 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:

    typescript
    // 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)
    Dica
    Piscina é a biblioteca mais popular para pooling de worker_threads. Alternativa nativa: gerencie um array de Workers manualmente com uma fila de tarefas. Piscina economiza esse boilerplate.

    Quando usar cluster vs worker_threads

    A escolha depende do tipo de operação que você quer paralelizar:

    bash
    # 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 parallelism

    Detectar bloqueio do event loop

    Event loop bloqueado é o maior problema de performance em Node.js. Detecte com métricas de lag:

    typescript
    // 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.

    Perguntas frequentes