FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memorysignifica que o V8 atingiu seu limite máximo. Existem duas situações muito diferentes por trás disso: uma carga de trabalho legítima que precisa de mais memória e um vazamento ou algoritmo carregando muito mais do que deveria. Aumentar o limite corrige o primeiro e oculta o segundo.
📋 Table of Contents
- Primeiro: que situação é esta?
- Correção rápida: aumente o limite de heap
- Causa 1: Lendo arquivos inteiros na memória
- Causa 2: resultados de consulta ilimitados
- Causa 3: Vazamentos Genuínos em Servidores de Longa Execução
- Encontrando um vazamento com instantâneos de heap
- Causa 4: Compilações ficando sem memória
- Limites de Docker e Contêiner
- Sequência Diagnóstica
- Perguntas Frequentes
- Conclusão
Primeiro: que situação é esta?
Responda isso antes de mudar qualquer coisa.
| Sintoma | Causa provável |
|---|---|
| Falha durante uma compilação ou um trabalho em lote grande | Precisa legitimamente de mais heap |
| O servidor trava após horas ou dias de atividade | Vazamento de memória |
| Falha em entradas grandes, correto em entradas pequenas | Carregando tudo na memória em vez de transmitir |
| Trava imediatamente em qualquer tamanho | Recursão ilimitada ou acumulação infinita |
Um servidor de longa duração que cresce constantemente apresenta um vazamento. Aumentar o limite apenas adia a queda.
Correção rápida: aumente o limite de heap
Apropriado para compilações e trabalhos em lote que realmente precisam de memória.
# Per invocation, in megabytes
node --max-old-space-size=4096 script.js
# For anything Node spawns, including build tools
export NODE_OPTIONS="--max-old-space-size=4096"
npm run build
{
"scripts": {
"build": "NODE_OPTIONS=--max-old-space-size=4096 next build"
}
}
Não defina isto acima da memória realmente disponível. Se o contêiner tiver 2 GB e você permitir um heap de 4 GB, o OOM killer do kernel encerrará o processo com SIGKILL — e você não receberá nenhum erro de JavaScript, apenas um código de saída 137, que é muito mais difícil de diagnosticar.
# Exit code 137 = 128 + 9 (SIGKILL) — killed by the OS, not by V8
docker inspect <container> --format='{{.State.ExitCode}}'
dmesg | grep -i "killed process"
Causa 1: Lendo arquivos inteiros na memória
A causa mais comum em código de processamento de dados.
// ❌ A 2GB file needs 2GB+ of heap, plus overhead for the string
import fs from 'node:fs/promises';
const content = await fs.readFile('huge.csv', 'utf8');
const lines = content.split('\n'); // now a second copy exists
// ✅ Constant memory regardless of file size
import fs from 'node:fs';
import readline from 'node:readline';
const stream = fs.createReadStream('huge.csv', { encoding: 'utf8' });
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
for await (const line of rl) {
await processLine(line);
}
O mesmo se aplica às respostas HTTP e aos resultados do banco de dados. Transmita-os; não os acumule.
// ❌ Buffers the entire response
const res = await fetch(url);
const buffer = await res.arrayBuffer();
// ✅ Pipes straight to disk
import { pipeline } from 'node:stream/promises';
import { Writable } from 'node:stream';
await pipeline(res.body, fs.createWriteStream('output.bin'));
Causa 2: resultados de consulta ilimitados
// ❌ Ten million rows, all resident at once
const users = await db.query('SELECT * FROM users');
for (const user of users) await sendEmail(user);
// ✅ Keyset-paginated batches — constant memory
let lastId = 0;
const BATCH = 1000;
while (true) {
const rows = await db.query(
'SELECT * FROM users WHERE id > $1 ORDER BY id LIMIT $2',
[lastId, BATCH]
);
if (rows.length === 0) break;
for (const user of rows) await sendEmail(user);
lastId = rows[rows.length - 1].id;
}
Melhor ainda, use um cursor de banco de dados onde seu driver suporte um – ele transmite linhas sem manter o conjunto completo de resultados em nenhum dos lados.
Causa 3: Vazamentos Genuínos em Servidores de Longa Execução
Quatro padrões são responsáveis pela maioria deles.
Um cache ilimitado. Um objeto simples ou mapa que só cresce.
// ❌ Grows forever
const cache = new Map();
function get(key) {
if (!cache.has(key)) cache.set(key, expensive(key));
return cache.get(key);
}
// ✅ Bounded with eviction
import { LRUCache } from 'lru-cache';
const cache = new LRUCache({ max: 5000, ttl: 1000 * 60 * 10 });
Ouvintes que nunca são removidos.
// ❌ Adds a listener on every request
app.get('/data', (req, res) => {
emitter.on('update', () => res.write('...'));
});
// ✅ Remove it when the request ends
app.get('/data', (req, res) => {
const onUpdate = () => res.write('...');
emitter.on('update', onUpdate);
res.on('close', () => emitter.off('update', onUpdate));
});
Node avisa sobre isso —MaxListenersExceededWarning é um indicador de vazamento, não um ruído a ser suprimido.
Temporizadores que nunca são apagados. CadasetInterval mantém seu fechamento e tudo o que ele faz referência vivo para sempre.
Fechamentos capturando objetos grandes. Um retorno de chamada que faz referência a um campo de um objeto enorme mantém todo o objeto acessível.
Encontrando um vazamento com instantâneos de heap
Adivinhar é lento. Tire fotos e compare.
# Start with the inspector attached
node --inspect server.js
# Open chrome://inspect in Chrome, click "inspect", go to the Memory tab
O procedimento: tirar um instantâneo, executar a carga de trabalho suspeita por um tempo, tirar um segundo instantâneo e, em seguida, classificar por “Delta” na visualização de comparação. Objetos que crescem e nunca encolhem entre os snapshots são o seu vazamento, e a árvore de retenção mostra exatamente o que os mantém vivos.
Você também pode acionar snapshots de dentro do processo, o que é útil na produção.
import v8 from 'node:v8';
import fs from 'node:fs';
process.on('SIGUSR2', () => {
const file = `/tmp/heap-${Date.now()}.heapsnapshot`;
fs.writeFileSync(file, v8.getHeapSnapshot());
console.log('Heap snapshot written to', file);
});
// Then: kill -SIGUSR2 <pid>
Registre o uso do heap continuamente para confirmar um vazamento antes de procurá-lo.
setInterval(() => {
const m = process.memoryUsage();
console.log({
rss: `${(m.rss / 1e6).toFixed(0)}MB`,
heapUsed: `${(m.heapUsed / 1e6).toFixed(0)}MB`,
heapTotal: `${(m.heapTotal / 1e6).toFixed(0)}MB`,
external: `${(m.external / 1e6).toFixed(0)}MB`,
});
}, 30_000);
Um padrão dente de serra é saudável – coleta de lixo recuperando memória. Uma escada que só sobe é um vazamento.
Causa 4: Compilações ficando sem memória
Compilações grandes de TypeScript, webpack ou Next.js consomem muita memória e frequentemente falham em contêineres de CI com limites modestos.
# Give the build more headroom
NODE_OPTIONS=--max-old-space-size=6144 npm run build
# TypeScript: incremental builds reuse previous work
tsc --incremental --noEmit
# Split very large builds into projects
tsc --build tsconfig.json
No CI, certifique-se de que o executor realmente tenha a memória que você está permitindo. Um limite de heap de 6 GB em um executor de 4 GB produz uma eliminação de OOM em vez de um erro útil.
Limites de Docker e Contêiner
O nó não dimensiona automaticamente seu heap para um limite de contêiner em todas as configurações, portanto, defina ambos explicitamente.
services:
api:
image: my-api
environment:
# Keep the heap below the container limit, leaving room for
# native allocations, buffers, and the runtime itself.
NODE_OPTIONS: "--max-old-space-size=1536"
deploy:
resources:
limits:
memory: 2G
A lacuna entre o limite do heap e o limite do contêiner é importante. Buffers, módulos nativos e o tempo de execução são alocados fora do heap JavaScript e--max-old-space-size não dá conta deles.
Sequência Diagnóstica
- Determine se é um erro V8 ou uma eliminação do sistema operacional – o código de saída 137 significa que o kernel fez isso.
- Registro
process.memoryUsage()ao longo do tempo. Subir monotonicamente significa um vazamento. - Se falhar apenas em entradas grandes, procure o carregamento de todo o arquivo ou de todo o conjunto de resultados.
- Se for um servidor de longa execução, tire duas capturas instantâneas de heap e compare os deltas.
- Verifique a árvore de retenção dos objetos em crescimento para descobrir o que os segura.
- Aumente o limite de heap apenas depois de estabelecer que o uso é legítimo.
Perguntas Frequentes
P: Qual é o limite de heap padrão?
R: Depende da versão do Node e da memória disponível do sistema, e as versões modernas dimensionam-no de forma mais sensata do que as mais antigas. Verifique o seu comnode -e "console.log(v8.getHeapStatistics().heap_size_limit / 1e6)".
P: Aumentar o tamanho máximo do espaço antigo é seguro?
R: Para compilações e trabalhos em lote com necessidades genuínas de memória, sim, desde que a máquina tenha memória. Para um servidor com vazamento, isso adia a falha e dificulta o diagnóstico.
P: Por que meu aplicativo trava com o código de saída 137 e nenhum erro?
R: O assassino OOM do kernel enviou SIGKILL, que não pode ser detectado. Seu limite de heap excede o limite do contêiner ou algo fora do heap está consumindo memória.
P: Posso forçar a coleta de lixo?
R: Com--expose-gc você pode ligarglobal.gc(), o que é útil para testar se a memória é realmente retida. Não é uma solução – se a memória não for recuperada, algo ainda faz referência a ela.
P: Os threads de trabalho ajudam?
R: Cada trabalhador recebe seu próprio heap, portanto, dividir o trabalho entre eles aumenta a capacidade total. Não corrige um vazamento; ele o distribui.
Conclusão
Comece classificando a falha:um erro de heap V8 em uma compilação geralmente significa que o trabalho realmente precisa de mais memória, enquanto o crescimento constante em um servidor de longa execução significa um vazamento. Transmita arquivos e pagine os resultados da consulta em vez de carregar tudo de uma vez, vincule cada cache com limites de tamanho e TTL, remova ouvintes e limpe temporizadores quando seu escopo terminar e use a comparação de instantâneos de heap para descobrir o que realmente está sendo retido. Aumentar--max-old-space-size somente depois de confirmar que o uso é legítimo e sempre mantenha-o confortavelmente abaixo do limite de memória do contêiner.
🔗 Share this article
✍️ Leave a Comment