Memory leak Node.js: como debugar em 6 passos
Descubra como debugar memory leaks em Node.js com um passo a passo objetivo: de sinais de alerta a heap snapshots e correção de referências.
Descubra como debugar memory leaks em Node.js com um passo a passo objetivo: de sinais de alerta a heap snapshots e correção de referências.
Memory leak em Node.js não aparece em testes rápidos. Ele se manifesta em produção, após horas ou dias de uso, quando o processo consome cada vez mais RAM até derrubar o serviço. Este guia mostra um caminho objetivo para identificar a causa, sem depender de ferramentas caras ou de tentativa e erro. Ao final, você terá um método reproduzível para diagnosticar e corrigir vazamentos.
Pré-requisitos
Antes de começar, verifique se você tem:
- Node.js instalado (versão 12 ou superior para acesso ao módulo
v8eprocess.memoryUsage()). - Acesso ao código em execução, de preferência em ambiente de staging que reproduza o cenário de produção.
- Uma carga de teste ou script que simule o uso real da aplicação.
Sem esses itens, o diagnóstico fica limitado a suposições. O objetivo é transformar o vazamento em um dado observável.
Passo 1: Confirme que há um vazamento
O primeiro erro comum é assumir que qualquer crescimento de memória é um leak. Em Node.js, o V8 aumenta o heap conforme a demanda, e isso não é necessariamente um problema. Para confirmar, monitore o uso de memória ao longo do tempo sob carga constante.
Use process.memoryUsage() em intervalos regulares e registre o valor de heapUsed e rss. Se o heapUsed crescer de forma contínua, mesmo após o GC rodar, há um indício forte de vazamento. Se o crescimento estabilizar, pode ser apenas o heap se ajustando.
Dica: rode o processo por pelo menos 24 horas ou até que a memória dobre em relação ao valor inicial. Um crescimento de 10% em uma hora pode ser normal; dobrar em um dia não é.
Passo 2: Capture heap snapshots
O heap snapshot é a fotografia do estado da memória em um dado momento. O Node.js expõe isso via v8.getHeapSnapshot(). O passo é capturar dois snapshots: um no início do processo, quando a memória está baixa, e outro depois de horas de uso ou quando o vazamento for evidente.
const v8 = require('v8'); const fs = require('fs');
const snapshot = v8.getHeapSnapshot(); snapshot.pipe(fs.createWriteStream('snapshot.heapsnapshot'));
Compare os dois arquivos. A diferença mostra quais objetos foram alocados e não liberados. Se um tipo de objeto específico domina a diferença, ele é o suspeito principal.
Erro comum: capturar apenas um snapshot. Um único snapshot não mostra crescimento, apenas o estado atual. Sem o comparativo, você não sabe o que é retido indevidamente.
Passo 3: Use o Chrome DevTools para analisar
Abra o Chrome e acesse chrome://inspect. Conecte ao processo Node.js em execução e vá para a aba "Memory". Carregue os snapshots capturados e use a comparação para filtrar por "Objects allocated between snapshots".
O DevTools mostra a árvore de retenção: quem segura a referência para o objeto. É aqui que você descobre o ponto exato do vazamento. Por exemplo, um Set que nunca é limpo, ou um EventEmitter com listeners acumulados.
Dica: preste atenção em closures e variáveis globais. Elas são as causas mais comuns de referências retidas em JavaScript.
Passo 4: Procure por listeners e timers
Listeners de eventos são a causa número um de memory leak em Node.js. Cada on() adiciona uma função à memória. Se você registra um listener dentro de uma função que é chamada repetidamente, sem remover, ele se acumula.
Use process.getActiveResourcesInfo() (Node 17+) para listar timers, sockets e handles ativos. Se o número de timers cresce sem parar, você tem um problema claro. A correção é sempre remover o listener com off() ou usar once() quando a intenção é ouvir uma única vez.
Exemplo: um servidor HTTP que adiciona um listener de data em cada request, sem remover, vaza memória a cada requisição.
Passo 5: Examine caches e variáveis globais
Caches são traiçoeiros. Um Map global que armazena resultados de consultas pode crescer indefinidamente se não houver limite. O mesmo vale para WeakMap quando usado incorretamente, embora ele seja mais seguro por natureza.
Revise o código em busca de variáveis declaradas sem let ou const no escopo global, ou objetos anexados ao global. Eles nunca são coletados enquanto o processo viver.
Dica: se o cache é necessário, defina um tamanho máximo ou um TTL. Um Map com limite de 1000 entradas evita o crescimento infinito sem sacrificar a performance.
Passo 6: Corrija e valide
Aplique a correção identificada, seja remover listeners, limpar caches ou ajustar escopo de variáveis. Depois, repita o monitoramento do Passo 1. O heapUsed deve se estabilizar ou crescer de forma previsível, sem dobrar.
Erro comum: corrigir um vazamento e não testar sob a mesma carga. O vazamento pode ser parcial, e a correção apenas reduz a velocidade, não elimina. Teste por pelo menos 48 horas em staging antes de promover para produção.
Checklist rápido
- [ ] Monitorei
heapUsedpor 24 horas e confirmei crescimento contínuo. - [ ] Capturei dois heap snapshots em momentos distintos.
- [ ] Analisei a diferença no Chrome DevTools e identifiquei o objeto retido.
- [ ] Verifiquei listeners, timers e caches globais.
- [ ] Corrigi a causa e validei sob carga por 48 horas.
FAQ
Como detectar memory leak em Node.js sem ferramentas externas?
Use process.memoryUsage() em intervalos regulares e registre heapUsed. Se o valor crescer continuamente sob carga constante, há indício de vazamento. Para confirmar, capture heap snapshots com v8.getHeapSnapshot() e compare os arquivos. O módulo nativo v8 e o process são suficientes para um diagnóstico inicial.
O que causa memory leak em Node.js?
As causas mais comuns são listeners de eventos não removidos, timers que nunca são limpos, caches sem limite e variáveis globais acidentais. Referências retidas em closures também aparecem com frequência. Em todos os casos, o padrão é o mesmo: um objeto que deveria ser coletado pelo GC permanece acessível por uma referência ativa.
Memory leak em Node.js é um problema de código ou de infraestrutura?
Na maioria dos casos, é um problema de código. A infraestrutura pode acelerar o vazamento, mas a causa raiz está em referências não liberadas no JavaScript. Exceções incluem módulos nativos mal escritos ou drivers de banco com bugs, mas esses são raros. Comece a investigação pelo código da aplicação.
Qual a diferença entre heapUsed e rss no Node.js?
heapUsed é a memória alocada pelo V8 para objetos JavaScript. rss (resident set size) é a memória total do processo, incluindo heap, stack e código nativo. Um vazamento de JavaScript aparece primeiro em heapUsed. Se rss cresce mas heapUsed fica estável, o problema pode estar em buffers nativos ou módulos C++.
Quando devo me preocupar com o crescimento de memória?
Quando o crescimento é contínuo e não estabiliza, mesmo após o garbage collector rodar. Um pico seguido de queda é normal. Um aumento progressivo que dobra a memória em menos de 24 horas sob carga constante é um sinal de alerta. Monitore também o tempo de resposta, pois vazamentos geralmente degradam a performance antes de derrubar o processo.
Gustavo Rennó
Colunista de tecnologia e produto
Acompanha a indústria de software de dentro. Escreve sobre produto, IA aplicada e o hype que não vira receita.
Ver todos os artigos →