Memory leaks em Node.js são como aquela conta que chega no fim do mês: você não percebe quando começa, mas quando vê, já virou um problema. O Node.js usa o V8, que tem um coletor de lixo eficiente, mas ele não é mágico. Se o seu código segura referências que não deveria, a memória vai crescendo até derrubar o processo. A boa notícia? Dá para achar e corrigir com método.
O que você vai conseguir com este guia: identificar sinais de vazamento, usar as ferramentas certas para capturar e comparar heap snapshots, e aplicar correções diretas no código. Pré-requisitos: uma aplicação Node.js rodando (versão 12 ou superior), acesso ao terminal e, de preferência, um ambiente de staging para testar sem medo.
Passo 1: Confirme que existe um vazamento de memória
Antes de sair caçando referências, certifique-se de que o problema é real. Um pico de memória pode ser só uma carga alta momentânea. Vazamento é o que cresce de forma contínua e não volta ao normal.
Um jeito rápido: monitore o uso de memória do processo com process.memoryUsage(). Rode sua aplicação e colete a métrica em intervalos regulares, por exemplo a cada 30 segundos, durante alguns minutos ou horas. Se o heapUsed (memória do heap usada) só sobe e nunca desce de forma significativa, você tem um vazamento.
Dica: crie um endpoint de debug em staging que retorne process.memoryUsage() para facilitar a coleta. Erro comum: confundir pico de memória com vazamento. Um gráfico em dente de serra é normal; uma escada crescente é o problema.
Passo 2: Capture heap snapshots para ver o que está na memória
O V8 permite tirar "fotos" do heap, ou seja, um retrato de todos os objetos alocados na memória. Com duas fotos em momentos diferentes, você compara e vê o que cresceu.
Use o módulo nativo v8 do Node.js:
const v8 = require('v8'); const fs = require('fs');
// Tira um snapshot e salva em arquivo const snapshot = v8.writeHeapSnapshot('snapshot-1.heapsnapshot');
Rode sua aplicação, tire um snapshot no início, deixe-a trabalhar por um tempo (ou execute uma carga de teste) e tire outro. Depois, abra os arquivos no Chrome DevTools: aba Memory, botão Load, e carregue o snapshot. O DevTools mostra a árvore de retenção, ou seja, quem está segurando cada objeto na memória.
Dica: use o modo Comparison no DevTools para destacar objetos que cresceram entre os snapshots. Erro comum: tirar snapshots cedo demais, sem dar tempo de a aplicação estabilizar ou executar os cenários que causam o vazamento.
Passo 3: Analise os retentores e encontre a referência culpada
O snapshot mostra objetos, mas o que importa é a cadeia de referências: o que faz um objeto não ser coletado. No DevTools, selecione um objeto com memória alta e olhe a seção Retainers. Você verá a árvore de quem referencia quem, até chegar a uma raiz global, como window no navegador ou global no Node.js.
Causas comuns: listeners de eventos não removidos, timers ou intervals sem clearInterval, caches que crescem sem limite, e variáveis globais acidentais. Por exemplo, se você usa setInterval para atualizar um cache e nunca limpa o intervalo, o objeto do cache fica preso para sempre.
Dica: procure por objetos que aparecem em múltiplos snapshots com o mesmo tamanho, mas que deveriam ter sido liberados após uma operação. Erro comum: ignorar closures. Uma função que captura variáveis do escopo pai pode segurar objetos indesejados se essa função for registrada como callback e nunca for removida.
Passo 4: Use o monitoramento contínuo para pegar vazamentos em produção
Debugging local é ótimo, mas vazamentos costumam aparecer sob carga real. Ferramentas como o inspector do Node.js (node --inspect) permitem capturar snapshots remotamente, mas isso não escala para produção. O ideal é integrar monitoramento de memória ao seu observability.
Você pode usar o process.memoryUsage() em um cron que envia métricas para um sistema de monitoramento (como Prometheus ou Datadog). Defina alertas para quando o heapUsed crescer de forma anormal ao longo de horas. O objetivo não é reagir ao pico, mas ver a tendência de crescimento.
Dica: capture um heap snapshot automaticamente quando a memória ultrapassar um limite, para ter o diagnóstico na hora do incidente. Erro comum: esperar o vazamento derrubar a aplicação para agir. Quando o processo morre, o snapshot se perde.
Passo 5: Corrija as causas típicas e teste novamente
A correção depende da causa, mas os suspeitos habituais são:
- Listeners de eventos: use
removeEventListenerouoffquando não precisar mais do listener. - Timers: sempre guarde o ID de
setInterval/setTimeoute chameclearInterval/clearTimeout. - Caches: defina um limite de tamanho ou use um TTL (tempo de vida) para expirar entradas.
- Variáveis globais: evite
global.minhaVariavel; use módulos com escopo local. - Streams e buffers: certifique-se de consumir completamente o corpo de requisições e respostas, especialmente com a Fetch API nativa. Se você não lê o body, o Node pode reter a memória.
Depois de corrigir, repita o Passo 1: monitore a memória e confirme que a curva se estabilizou. Se o problema persistir, volte aos snapshots e procure por novos retentores.
Dica: escreva um teste de estresse que execute a operação suspeita em loop e verifique se a memória retorna ao baseline após um global.gc(). Erro comum: corrigir uma causa e achar que resolveu, sem validar com snapshots antes e depois.
Checklist final do que fizemos
- [ ] Confirmei que há vazamento com monitoramento de
process.memoryUsage() - [ ] Capturei heap snapshots em momentos diferentes
- [ ] Analisei os retentores no Chrome DevTools
- [ ] Identifiquei a referência que segura objetos sem necessidade
- [ ] Corrigi a causa (listener, timer, cache, global, stream)
- [ ] Validei a correção com novo monitoramento
FAQ
Como detectar memory leak em Node.js?
Use process.memoryUsage() para monitorar o heapUsed ao longo do tempo. Se a memória crescer de forma contínua e não voltar ao normal, capture heap snapshots com v8.writeHeapSnapshot() e compare-os no Chrome DevTools para achar os objetos retidos.
O que causa memory leak em Node.js?
As causas mais comuns são listeners de eventos não removidos, timers sem clearInterval/clearTimeout, caches sem limite, variáveis globais e streams ou corpos de requisição não consumidos. Essas referências impedem o coletor de lixo de liberar memória.
Como usar heap snapshot para debugar?
Tire um snapshot do heap antes e depois de executar o cenário que causa o vazamento, usando v8.writeHeapSnapshot(). Carregue os arquivos no Chrome DevTools e use o modo Comparison para ver quais objetos cresceram e quem os referencia.
O que é heapUsed no Node.js?
heapUsed é uma métrica do V8 que indica a quantidade de memória atualmente usada pelo heap de objetos JavaScript. Ele faz parte do objeto retornado por process.memoryUsage(), junto com outras métricas como heapTotal e external.
Como evitar memory leaks em produção?
Além de corrigir as causas comuns, configure monitoramento contínuo de memória com alertas de tendência de crescimento. Capture snapshots automáticos quando a memória ultrapassar um limite para facilitar o diagnóstico. Teste com carga real antes de lançar.
Memory leak em Node.js é comum?
Sim, especialmente em aplicações de longa duração que lidam com muitas conexões ou eventos. O Node.js não vaza memória por si só, mas o uso descuidado de callbacks, timers e caches pode criar referências acidentais que impedem a coleta de lixo.
Agora que você sabe o caminho, a próxima vez que a memória subir, não vai ser surpresa. É só seguir o roteiro: monitorar, fotografar, comparar e corrigir. A internet não perdoa, mas a gente ri junto quando o vazamento vira história para contar.