🌐 Detecting your location…

Como corrigir o erro ‘JavaScript heap sem memória’ em Node.js

⏱️8 min read  ·  1,600 words

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.

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

  1. 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.
  2. Registroprocess.memoryUsage() ao longo do tempo. Subir monotonicamente significa um vazamento.
  3. Se falhar apenas em entradas grandes, procure o carregamento de todo o arquivo ou de todo o conjunto de resultados.
  4. Se for um servidor de longa execução, tire duas capturas instantâneas de heap e compare os deltas.
  5. Verifique a árvore de retenção dos objetos em crescimento para descobrir o que os segura.
  6. 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.

MD Rafikul Islam

Written by

MD Rafikul Islam is a software developer and the editor of TechPulse. He writes about developer tooling, hardware, and the practical decisions that come up in day-to-day engineering work — which laptop to buy, which framework to commit to, why a build broke at 2am. He tests the tools he writes about and says plainly when something is not worth the money. Corrections and corrections requests are welcome at rony.yf25@gmail.com.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা