Objetivos de aprendizado
Depois de ler este artigo, você vai conseguir:
- Diferenciar um erro de OOM do runtime de um encerramento silencioso pelo kernel
- Descobrir o que está consumindo memória em Node.js, Python ou Java
- Corrigir a causa e definir limites para o problema não voltar
Sua aplicação roda bem por uma hora, um dia ou uma semana, e de repente some e começa de novo. O código não mudou e não há stack trace. Esse padrão, um reinício sem erro de código, é a marca clássica de um crash por falta de memória.
Memória é o único recurso que não se degrada aos poucos. Falta de CPU deixa a aplicação mais lenta. Falta de memória encerra o processo.
O que é um erro de falta de memória (OOM)?#
Todo processo precisa de RAM para o código, os dados e o runtime que o executa. Um container tem um limite de memória. Quando a aplicação tenta usar mais do que esse limite e o sistema não consegue liberar nada, algo tem que ceder.
O kernel do Linux documenta isso para cgroups: se o uso de memória de um cgroup chega ao memory.max e não pode ser reduzido, o OOM killer é acionado naquele cgroup. O Docker diz o mesmo em termos simples: por padrão, se ocorre um erro de OOM, o kernel mata processos dentro do container.
Na prática, o nome "OOM" cobre duas falhas diferentes:
- OOM do runtime. O runtime da linguagem percebe que acabou o heap e aborta com uma mensagem que você consegue ler no log.
- OOM kill do kernel. O kernel encerra o processo de fora. A aplicação não consegue capturar isso e não escreve mensagem nenhuma.
Como saber se é um crash por OOM?#
Confira estes sinais, nesta ordem.
1. A mensagem no log. Um OOM do runtime no Node.js tem esta cara:
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memoryO meio da mensagem varia (você também pode ver Ineffective mark-compacts near heap limit), mas ela termina com JavaScript heap out of memory.
O Python lança MemoryError e o Java lança java.lang.OutOfMemoryError (por exemplo Java heap space). Se você vê um desses, é um OOM do runtime, e o stack trace mostra onde aconteceu.
2. Um log que simplesmente para. Num kill do kernel, a última linha é uma saída comum, e a seguinte já é a mensagem de inicialização, sem erro nenhum no meio. Junto com o próximo sinal, isso é quase conclusivo.
3. Código de saída 137. Se suas ferramentas mostram o código de saída, 137 significa 128 mais o sinal 9 (SIGKILL), pela convenção do shell. É o que aparece quando o kernel mata um processo de container.
4. O gráfico de memória. Olhe a RAM ao longo do tempo. Uma linha que sobe e cai para perto de zero a cada reinício indica que a aplicação bateu no limite. Se ainda não sabe onde encontrar essa saída, veja como ler os logs da sua aplicação.
Se o gráfico está estável e baixo na hora do crash, provavelmente não é memória. Procure uma exceção não tratada.
O que faz uma aplicação ficar sem memória?#
Da causa mais comum para a menos comum:
- Vazamento de memória. Algo mantém referências que deveria soltar: um array que só cresce, um cache sem limite de tamanho, um event listener adicionado a cada requisição, um map indexado por ID de usuário que nunca é limpo.
- Carregar demais de uma vez. Ler um arquivo grande inteiro ou uma tabela inteira do banco para a memória, em vez de usar stream ou paginação.
- Um limite simplesmente pequeno demais. A aplicação está saudável, mas o pico normal dela é maior do que a memória recebida. Veja quanta RAM e CPU sua aplicação precisa.
- Limite do runtime abaixo do limite do container. O Node.js e a JVM têm tetos próprios de heap, separados do container. A memória fora do heap (buffers, bibliotecas nativas, pilhas de threads) conta para o container mas não para o heap, então o processo pode ser morto com o heap parecendo normal.
- Picos de tráfego. Muitas requisições simultâneas, cada uma segurando um objeto grande. Bots em muitos servidores também passam por isso (veja quanta RAM um bot do Discord precisa).
Como descobrir o que está usando a memória?#
Meça antes de chutar. O objetivo é ver quais objetos crescem.
Node.js#
Registre o uso de memória a cada poucos segundos. O rss é o processo inteiro e é o que conta para o limite do container:
// memory-log.js: carregue primeiro no seu arquivo de entrada (require ou import)
const mb = (n) => Math.round(n / 1024 / 1024);
setInterval(() => {
const m = process.memoryUsage();
console.log(`rss=${mb(m.rss)}MB heapUsed=${mb(m.heapUsed)}MB external=${mb(m.external)}MB`);
}, 30_000).unref();Se o heapUsed sobe junto com o rss, é um vazamento em JavaScript. Se o rss sobe mas o heapUsed fica estável, olhe para buffers, módulos nativos ou streams grandes.
Para ter um heap snapshot logo antes do crash, inicie o Node com --heapsnapshot-near-heap-limit. A documentação do Node.js descreve que ele grava um heap snapshot do V8 no disco quando o uso do heap se aproxima do limite:
node --max-old-space-size=400 --heapsnapshot-near-heap-limit=2 index.jsAbra o arquivo .heapsnapshot gerado no Chrome DevTools (aba Memory) e ordene por retained size. Tirar snapshots também custa tempo e memória, então use isso para investigar, não como configuração permanente.
Python#
A biblioteca padrão tem o tracemalloc. Inicie cedo, tire um snapshot depois e imprima as linhas que mais alocaram:
import tracemalloc
tracemalloc.start()
leaky = [bytes(1000) for _ in range(50_000)] # faz o papel da sua carga de trabalho real
snapshot = tracemalloc.take_snapshot()
for stat in snapshot.statistics("lineno")[:10]:
print(stat)O rastreamento adiciona custo, e um snapshot só cobre as alocações feitas depois do start(), então ligue-o enquanto depura.
Java#
Peça à JVM que grave um heap dump quando lançar OutOfMemoryError. Por padrão isso vem desligado:
java -Xmx384m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./dumps/ -jar app.jarAbra o arquivo .hprof numa ferramenta como Eclipse MAT ou VisualVM e procure os maiores objetos retidos.
Como corrigir um crash por OOM?#
Ajuste a correção à causa.
- Vazamento: remova a referência. Limite todo cache (um número máximo de entradas e um tempo de vida), remova os event listeners que você adiciona e pare de guardar dados por usuário para sempre.
- Dados demais de uma vez: use stream nos arquivos, pagine as consultas e processe em lotes, em vez de montar uma lista gigante.
- Limite pequeno demais: aumente a memória da aplicação para o pico medido mais 30% a 50% de folga, como explica o artigo de dimensionamento acima.
- Teto do runtime desalinhado: defina o teto do heap abaixo do limite do container, para que o runtime falhe com um erro legível em vez de um kill silencioso. No Node.js, a documentação sugere, para uma máquina de 2 GiB, definir
--max-old-space-sizecomo 1536 para deixar espaço para outros usos. Aplique a mesma proporção ao seu limite. No Java, defina-Xmxexplicitamente, já que o padrão é escolhido em tempo de execução a partir da configuração do sistema. - Picos: limite a concorrência, adicione uma fila ou leve o trabalho pesado (processamento de imagem, exportações grandes) para um processo separado.
Aumentar o limite só é uma correção válida quando o uso é estável. Se o gráfico sobe para sempre, um limite maior só faz o crash acontecer mais tarde e ficar mais difícil de reproduzir.
Como evitar que volte a acontecer?#
- Registre a memória ao longo do tempo, como nos trechos acima, e olhe os números depois de cada release.
- Defina tetos de heap explícitos para que as falhas sejam barulhentas.
- Faça testes de carga com dados realistas antes de lançar, não com um banco vazio.
- Crie um alerta para quando a memória ficar acima de uns 80% do limite por muito tempo, usando um monitor externo de uptime ou de métricas.
- Reinicie em horário fixo só como paliativo enquanto corrige um vazamento conhecido, nunca como a correção.
Como fazer isso na Square Cloud#
Na Square Cloud, cada aplicação roda num container isolado com a própria alocação de memória, e a RAM do plano é um pool que você divide entre suas aplicações. O mínimo é 256 MB para uma aplicação de fundo, como um bot, e 512 MB para uma aplicação web, e uma única aplicação pode usar até 32 GB.
Quando uma aplicação fica sem memória, ela para e continua parada: o reinício automático (ligado por padrão no deploy pelo painel) só cobre um processo que encerra com erro, então você aumenta a RAM e inicia de novo. Os logs em tempo real no painel, na CLI e na extensão do VS Code guardam a saída de antes da parada, então dá para ler as últimas linhas antes do crash. As métricas de CPU, RAM e rede são coletadas a cada 5 minutos e guardadas por 24 horas, o que pode mostrar o desenho de subida e queda de um vazamento que derruba a aplicação dentro de um dia.
Para corrigir um limite pequeno demais, mude a RAM da aplicação nas configurações dela ou pela API. Isso é escala vertical, feita por você: não há autoscaling. Os demais recursos de logs e métricas estão na página de recursos.
Deploy
Envie pelo GitHub, pela CLI ou em ZIP e publique em segundos
Perguntas frequentes
O que significa o código de saída 137?
Pela convenção do shell, 137 é 128 mais 9, ou seja, o processo foi encerrado por SIGKILL. Num container, esse é o sinal típico de um OOM kill, mas qualquer outro encerramento forçado gera o mesmo código, então confirme no gráfico de memória.
Por que não aparece nenhum erro no log quando a aplicação morre?
Quando o kernel mata um processo por passar do limite de memória, o processo não tem chance de registrar nada. Você só vê o log parar de repente, seguido de um reinício.
Vazamento de memória é a mesma coisa que erro de OOM?
Não. Vazamento é memória que cresce e nunca é liberada. O erro de OOM é o resultado quando o uso chega ao limite. O vazamento é uma das causas, mas uma aplicação também pode bater no limite carregando dados demais de uma vez.
Basta contratar mais RAM?
Se o uso é estável e fica só um pouco acima do limite, mais RAM é a correção certa. Se o uso sobe sem parar até a aplicação cair, mais RAM só adia o crash e você precisa corrigir o vazamento.
Mais em Deploy
Ver todos os 33 artigos- O que são variáveis de ambiente (e por que usar)Variáveis de ambiente são pares chave-valor que o sistema entrega ao processo. Veja como ler no Node.js e Python e por que tokens não ficam no código.
- O que é deploy (e o que acontece quando você faz um)Deploy é o processo de levar o código de uma aplicação para um servidor, prepará-lo e colocá-lo no ar. Veja as etapas, os métodos e como voltar atrás.
- Quanta RAM e CPU minha aplicação precisa?Dimensione RAM e CPU medindo o pico real da aplicação e somando uma folga de 30%. Veja como medir em Node.js, Python e Java e o que fazer quando faltar.
- Deploy automático com GitHub ActionsDeploy automático com GitHub Actions envia seu código para o servidor a cada push. Veja o workflow passo a passo, secrets, testes antes do deploy e erros comuns.