O que significa
Quando você define resources.limits.memory, o kubelet configura esse valor como teto no cgroup do container. Se o processo tentar usar mais do que isso, o kernel aciona o OOM killer e encerra o processo com SIGKILL, sinal 9. O container termina com exit code 137 (128 + 9) e o kubelet registra Reason: OOMKilled. Com a restartPolicy padrão (Always), o container é reiniciado; se estourar de novo, entra em CrashLoopBackOff. Fonte: documentação do Kubernetes, "Assign Memory Resources to Containers and Pods", seção "Exceed a Container's memory limit".
Um detalhe que confunde: o request de memória não protege contra OOMKilled. O request serve ao scheduler para escolher o nó; o limite é o que o kernel aplica. Um container com request 64Mi e limit 128Mi morre ao passar de 128Mi, e não de 64Mi. Sem limit definido, ele pode usar toda a memória livre do nó e, aí, quem age é a eviction por pressão no nó, que é outro erro (Evicted).
Causas comuns
O limits.memory foi copiado de um tutorial ou de outro serviço. A aplicação carrega cache, catálogo ou pool de conexões na subida e passa do teto em segundos.
Versões antigas da JVM (anteriores ao Java 10 e ao 8u191) dimensionam o heap pela memória do nó, não do container. Nas atuais, o heap respeita o limite, mas heap não é tudo: metaspace, threads e buffers diretos também ocupam memória. O Node dimensiona o heap por heurística própria, que pode não bater com o limite do container.
Cache sem teto, listeners não removidos, conexões não fechadas. O gráfico de 30 dias sobe em serra: cresce até o limite, morre, recomeça. Subir o limite só espaça as mortes.
Relatório da meia-noite, reindexação, importação de planilha. O uso médio cabe no limite; o pico do job não. Ou o job vai para um Job/CronJob com limite próprio, ou o limite acompanha o pico.
Agentes de log, proxies de service mesh e init containers têm os próprios limits. Um sidecar com pouca memória morre ao receber uma rajada de logs, e o pod inteiro parece instável.
Diagnóstico em 4 comandos
Saídas reais de um pod de teste que tenta alocar 150M com limite de 128Mi.
1 · o STATUS já diz, por alguns segundos
kubectl -n loja get pod checkout-oom NAME READY STATUS RESTARTS AGE
checkout-oom 0/1 OOMKilled 3 (52s ago) 75s Entre um reinício e outro o STATUS alterna com CrashLoopBackOff. O registro que não some é o do último término:
2 · confirme o motivo do último término
kubectl -n loja get pod checkout-oom \
-o jsonpath='{.status.containerStatuses[0].lastState}' | jq . {
"terminated": {
"exitCode": 137,
"finishedAt": "2026-09-25T22:55:22Z",
"reason": "OOMKilled",
"startedAt": "2026-09-25T22:55:22Z"
}
} 3 · veja o limite configurado e o histórico de reinícios
kubectl -n loja describe pod checkout-oom | grep -E -A4 "Last State|Limits|Requests|Restart Count" Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Restart Count: 3
Limits:
cpu: 500m
memory: 128Mi
Requests:
cpu: 100m
memory: 64Mi 4 · os eventos não citam OOMKilled
kubectl -n loja describe pod checkout-oom | sed -n '/Events:/,$p' Normal Started 29s (x4 over 70s) kubelet Container started
Warning BackOff 28s (x4 over 66s) kubelet Back-off restarting failed container checkout in pod checkout-oom_loja(...) No nosso teste, os eventos do pod mostraram só BackOff: quem mata é o kernel, e o motivo fica no Last State. Filtrar eventos por "OOM" não encontra nada. Se aparecer Unhealthy seguido de Killing, é a liveness probe que também está reiniciando o container, e o último término pode trazer outro exit code (0 ou 143, conforme o processo trate o SIGTERM).
Para comparar com o uso real, kubectl top pod --containers mostra o consumo do instante (precisa do metrics-server); o pico está no seu Prometheus ou Grafana.
A correção
- Pegue o pico de memória de um período representativo (no Prometheus:
max_over_time(container_memory_working_set_bytes[7d]), filtrando pelo container). - Defina
limits.memoryacima do pico, com folga para crescimento. A documentação não prescreve um fator: escolha um e registre o motivo em comentário no manifesto. - Suba
requests.memorypara o uso típico. Se request = limit em CPU e memória para todos os containers, o pod é da classe Guaranteed, a última a ser despejada por pressão no nó (documentação do Kubernetes, "Pod Quality of Service Classes"). - Faça o runtime caber no limite: JVM com
-XX:MaxRAMPercentage, Node com--max-old-space-size, .NET comDOTNET_GCHeapHardLimitPercent.
containers:
- name: checkout
image: registry.loja.internal/checkout:2.14.0
resources:
requests:
cpu: 250m
memory: 640Mi
limits:
cpu: 500m
memory: 800Mi
env:
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=75.0" avisoSubir o limite trata o sintoma. Se o pico cresce a cada semana, é vazamento de memória, e o próximo OOMKilled é questão de tempo. Compare o pico de uma semana com o de um mês.
Como aparece no Kubepier
O Kubepier mostra os eventos do pod com os avisos primeiro, com filtro por tipo, namespace e motivo. O diagnóstico por IA explica o erro a partir dos eventos e do log, em português, com a sua chave de API (Anthropic ou OpenAI). Abaixo, uma ilustração com dados fictícios.
Causa provável: o container tenta usar 150M com limite de 128Mi e o kernel o mata na subida. Evidência: Last State OOMKilled com exit code 137 e 3 reinícios em 75 s; os eventos mostram só BackOff. Correção sugerida: subir limits.memory acima do pico medido ou reduzir o que a aplicação carrega na memória.