Heap snapshots registram os objetos existentes no heap do V8 e as referências que os mantêm vivos. Em aplicações Node.js, eles ajudam a investigar vazamentos de memória, crescimento de caches, listeners não removidos, closures retidas, buffers acumulados e estruturas que continuam alcançáveis mesmo depois de perder utilidade.
Um snapshot pode ser carregado na aba Memory do Chrome DevTools. É possível analisar tamanho superficial, tamanho retido, caminhos até a raiz do garbage collector e diferenças entre duas capturas.
Aviso de produção
Criar um heap snapshot interrompe o trabalho da thread principal. Dependendo do heap, a operação pode levar mais de um minuto. A captura também é construída em memória e pode dobrar temporariamente o consumo, causando OOM e encerramento do processo.
Em produção, capture apenas de uma réplica que pode falhar sem afetar disponibilidade. Remova-a do balanceador, garanta espaço em disco e memória, monitore a operação e esteja preparado para reiniciar.
Sinais de vazamento
Memória crescente nem sempre significa leak. O V8 aumenta o heap, mantém espaço reservado e executa GC em ciclos. Investigue quando:
- heapUsed cresce continuamente após GCs completos;
- RSS não estabiliza com carga constante;
- latência de GC aumenta;
- processos reiniciam por OOM;
- uma rota específica aumenta memória a cada chamada;
- objetos de domínio permanecem após o fluxo terminar.
Métricas básicas
import process from 'node:process';
setInterval(() => {
const memory = process.memoryUsage();
console.log({
rss: memory.rss,
heapTotal: memory.heapTotal,
heapUsed: memory.heapUsed,
external: memory.external,
arrayBuffers: memory.arrayBuffers,
});
}, 10_000).unref();heapUsed representa objetos JavaScript. external e arrayBuffers podem crescer por Buffers e bibliotecas nativas. Um heap snapshot não explica sozinho toda a RSS.
Captura pelo Inspector
Localmente:
node --inspect src/server.jsAbra chrome://inspect, conecte ao processo, acesse Memory e escolha “Take heap snapshot”. Não exponha a porta do inspector à internet; ela permite controle do processo.
Captura por sinal
Inicie:
node --heapsnapshot-signal=SIGUSR2 dist/server.jsDepois:
kill -USR2 PIDO Node grava um arquivo .heapsnapshot no diretório atual. Garanta permissão de escrita e nomeie o diretório de trabalho de forma previsível.
writeHeapSnapshot
import { writeHeapSnapshot } from 'node:v8';
const filename = writeHeapSnapshot();
console.log(`Snapshot criado em ${filename}`);É possível escolher arquivo:
const filename = writeHeapSnapshot(
`/var/diagnostics/heap-${process.pid}-${Date.now()}.heapsnapshot`,
);Não exponha um endpoint HTTP público que chama essa função. Um atacante poderia bloquear a aplicação e esgotar disco ou memória.
Endpoint administrativo seguro
Se precisar de gatilho remoto, use canal administrativo isolado, autenticação forte, allowlist, rate limit e auditoria. Uma alternativa melhor é sinal do sistema operacional executado por operador autorizado.
Captura por Inspector Protocol
O protocolo do inspector permite iniciar a captura externamente e receber chunks. É útil em automações, mas a porta continua altamente privilegiada. Vincule em loopback, use túnel seguro e feche após diagnóstico.
Preparando uma comparação limpa
Para encontrar leak:
- inicie aplicação e conclua bootstrap;
- aqueça pools, rotas e caches normais;
- execute o fluxo suspeito algumas vezes;
- force uma pausa para GC natural e estabilização;
- tire snapshot A;
- repita apenas o fluxo suspeito muitas vezes;
- tire snapshot B;
- compare B contra A.
Evite executar tarefas não relacionadas entre capturas, pois o diff ficará cheio de ruído.
Carregando no Chrome DevTools
Na aba Memory:
- carregue o snapshot antigo;
- carregue o novo;
- selecione o novo;
- mude de Summary para Comparison;
- ordene por delta positivo ou retained size;
- expanda retainers.
Shallow size e retained size
Shallow size é o espaço do próprio objeto. Retained size é a memória que poderia ser liberada se o objeto e tudo que depende exclusivamente dele fossem coletados.
Um pequeno array pode reter uma árvore enorme. Priorize retained size e caminhos de retenção, não apenas quantidade de instâncias.
Distance e GC roots
Distance indica distância até uma raiz do GC. Objetos ligados a globals, módulos, timers, sockets e closures podem permanecer vivos indefinidamente. O painel de retainers mostra a cadeia que impede coleta.
Detached objects
Em aplicações servidoras, “detached DOM trees” normalmente não são relevantes, mas frameworks de SSR ou bibliotecas que simulam DOM podem reter árvores. Procure coleções que deveriam ter sido descartadas.
Listeners não removidos
function acompanhar(socket) {
const onData = (data) => processar(data);
socket.on('data', onData);
return () => socket.off('data', onData);
}Se o cleanup não é chamado, EventEmitter retém callback e tudo capturado pela closure.
Timers
const interval = setInterval(() => atualizarCache(contexto), 1000);
function stop() {
clearInterval(interval);
}Timers mantêm callbacks vivos. Use unref() quando o timer não deve impedir encerramento, mas isso não remove referências durante a vida do processo.
Caches sem limite
const cache = new Map();
export function getUser(id) {
if (!cache.has(id)) cache.set(id, carregar(id));
return cache.get(id);
}Um Map global sem TTL ou limite cresce para sempre. Use LRU, TTL, tamanho máximo e métricas de entradas.
Closures
Uma callback pode capturar objeto grande:
function agendarRelatorio(dataset) {
queue.push(() => gerar(dataset));
}Enquanto a fila retém a função, todo o dataset permanece vivo. Capture somente identificadores ou persista o dado fora do heap.
Promessas pendentes
Operações que nunca resolvem podem reter contexto, timeouts e respostas. Aplique timeout, cancelamento e cleanup em finally.
AsyncLocalStorage
Contextos assíncronos mal encerrados podem aumentar retenção. Não armazene payloads grandes, objetos request completos ou buffers no store. Guarde IDs pequenos.
Buffers e memória externa
Se RSS cresce mas heap snapshots não mostram crescimento proporcional, investigue Buffers, ArrayBuffers e addons nativos. Monitore external e arrayBuffers. Use heap profiler, allocation sampling e ferramentas do sistema.
Forçar GC em laboratório
Para diagnóstico local:
node --expose-gc test-leak.jsglobal.gc?.();Não dependa de GC manual em produção. Use apenas para reduzir ruído em experimento controlado.
Heap perto do limite
Node pode gerar snapshots automaticamente:
node --heapsnapshot-near-heap-limit=3 dist/server.jsIsso ajuda em OOM, mas aumenta pressão de memória e disco. Teste antes e configure coleta dos arquivos.
Limite de heap
node --max-old-space-size=2048 dist/server.jsAumentar o limite não corrige leak. Pode apenas adiar o crash e aumentar pausas de GC. Ajuste com base em memória do container e observabilidade.
Containers e Kubernetes
Em Kubernetes:
- capture de uma réplica isolada;
- grave em volume com espaço;
- copie o arquivo com canal seguro;
- não ultrapasse memory limit;
- remova o pod após captura se necessário;
- evite snapshot simultâneo de todas as réplicas.
Dados sensíveis
Heap snapshots podem conter tokens, senhas, dados pessoais, payloads e chaves. Trate-os como segredo de alta sensibilidade:
- criptografe em trânsito e repouso;
- limite acesso;
- defina retenção curta;
- não envie a serviços públicos;
- apague após análise;
- registre auditoria.
Tamanho dos arquivos
Snapshots podem ser maiores que o heap e demorar para carregar. Use estação com memória suficiente. Não abra vários arquivos gigantes ao mesmo tempo sem necessidade.
Allocation sampling
Heap snapshots mostram estado. Allocation sampling mostra onde objetos são alocados ao longo do tempo com menor overhead. Use o profiler quando precisa localizar fonte de alocação, e snapshots para retenção.
Testando uma correção
Após corrigir:
- repita a mesma carga;
- capture snapshots no mesmo intervalo;
- compare crescimento;
- observe heapUsed após GC;
- execute soak test;
- valide que cache e throughput permanecem corretos.
Erros comuns
- capturar no único processo de produção;
- confundir RSS com heap;
- comparar snapshots com cargas diferentes;
- olhar somente contagem de objetos;
- ignorar retainers;
- salvar em disco sem espaço;
- expor endpoint de captura;
- compartilhar arquivo sem proteção;
- aumentar heap em vez de corrigir leak.
Fluxo recomendado
Comece por métricas, reproduza com carga controlada, aqueça, capture dois snapshots, compare retained size e caminhos de retenção, corrija e valide com soak. Combine com carga em k6 no Node.js, benchmark em Autocannon, profiling contínuo em Pyroscope e métricas em Prometheus.
Consulte o guia oficial de heap snapshots do Node.js e a referência oficial de writeHeapSnapshot.




