OOMKilled in Kubernetes: 6 Ursachen, kubectl-Workflow, Right-Sizing
Pod stirbt mit Exit 137? Die 6 Ursachen für OOMKilled in Kubernetes - mit kubectl-Befehlen, JVM-Fallen und Decision-Tree zum Right-Sizing in unter 10 Minuten.
TL;DR - OOMKilled ist kein Bug, sondern eine Diagnose: der Kernel-OOM-Killer hat den Container beendet, weil er das Memory-Limit überschritten hat. Limits zu verdoppeln ist die teuerste Lösung - oft falsch. Dieser Workflow findet die echte Ursache in unter 10 Minuten: bestätigen, messen, mit Limit vergleichen, gezielt fixen.
🔖 Nur die Befehle? Hier ist der interaktive OOMKilled-Cheatsheet - mit Copy-Buttons, Runtime-Limits für JVM/Node/Go/Python und Druck-Ansicht. Lesezeichen empfohlen.
Was OOMKilled wirklich bedeutet
OOMKilled ist die Diagnose, nicht der Bug. Der Ablauf:
- Container alloziert Memory bis er sein cgroup-Memory-Limit erreicht
- Kernel sendet
SIGKILLan den Container-Hauptprozess - Container-Runtime meldet Exit-Code 137 (128 + Signal 9)
- Kubelet schreibt
Reason: OOMKilledin den Pod-Status - Restart-Policy entscheidet:
Always→ Restart,OnFailure→ Restart,Never→ Pod bleibt Failed
Wichtig: der OOM-Killer ist eine Linux-Kernel-Entscheidung, kein Kubernetes-Feature. Kubernetes setzt nur das cgroup-Limit, der Kernel zieht es durch. Heißt auch: ein OOMKill ist immer hart und sofort. Kein Graceful Shutdown, kein SIGTERM, kein Pre-Stop-Hook. Der Prozess ist in derselben Mikrosekunde tot, in der das Limit gerissen wird.
Der 4-Schritt-Workflow
Egal welche Ursache - diese vier Schritte laufen immer.
Schritt 1: Bestätigen, dass es wirklich OOMKilled war
kubectl describe pod <name> | grep -A 5 "Last State"
Output bei echtem OOMKill:
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Started: Thu, 08 May 2026 09:14:22 +0200
Finished: Thu, 08 May 2026 09:18:47 +0200
Wenn Reason: Error und Exit Code: 137 steht, war es nicht der cgroup-OOM-Killer - sondern ein externer SIGKILL (Liveness-Probe-Death-Spiral, Node-Eviction durch Memory-Pressure, oder ein Sidecar). Andere Ursache, anderer Workflow.
Schritt 2: Realen Verbrauch messen
kubectl top pod <name> --containers
Das ist der Live-Wert. Spannender ist aber der Peak:
# cgroups v2 (Kubernetes 1.25+, alle modernen Distros)
kubectl exec <pod> -- cat /sys/fs/cgroup/memory.peak
# cgroups v1 (Legacy)
kubectl exec <pod> -- cat /sys/fs/cgroup/memory/memory.max_usage_in_bytes
memory.peak zeigt das Maximum seit Container-Start in Bytes. Das ist die einzige Zahl, die für Limit-Berechnungen zählt.
Schritt 3: Mit Limit vergleichen
kubectl describe pod <name> | grep -A 3 "Limits\|Requests"
Oder strukturiert per JSON:
kubectl get pod <name> -o jsonpath='{range .spec.containers[*]}{.name}: {.resources}{"\n"}{end}'
Drei Szenarien:
- Peak ≈ Limit → Limit ist zu klein oder echte Spitze. Ursachen 1, 3 oder 4.
- Peak ≪ Limit, aber trotzdem OOM → Node-OOM. Ursache 6.
- Peak steigt monoton über Tage → Memory-Leak. Ursache 2.
Schritt 4: Gezielt fixen
Erst nachdem Schritt 1-3 abgeschlossen sind. Wer ohne Messung am Limit dreht, bezahlt entweder durch unnötige Node-Kosten oder durch den nächsten Crash in 6 Stunden.
Die 6 häufigsten Ursachen
1. Limit zu niedrig gesetzt
Häufigster Fall in der Praxis. Jemand hat das Limit nach Bauchgefühl gesetzt, die App braucht real mehr.
Diagnose: Peak liegt knapp unter oder am Limit, Verbrauch ist stabil (kein Wachstum), kein Memory-Leak.
Fix: Limit auf Peak × 1,3 setzen, Request auf den durchschnittlichen Verbrauch.
resources:
requests:
memory: "512Mi" # durchschnittlicher Verbrauch
limits:
memory: "1Gi" # Peak * 1.3, nicht mehr
2. Memory-Leak in der Anwendung
Verbrauch wächst monoton über Stunden oder Tage, dann OOMKill, Restart, Verlauf beginnt von vorn.
Diagnose: kubectl top pod über mehrere Stunden mitschneiden, Sägezahn-Muster ist eindeutig.
# 5 Minuten alle 10 Sekunden mitloggen
while true; do kubectl top pod <name> --no-headers; sleep 10; done | tee mem.log
Fix: Profiler im Code (pprof für Go, jmap/heap-dump für Java, —inspect für Node.js). Limit zu erhöhen ist hier der teuerste Workaround - der Pod stürzt nur später ab.
3. Burst-Last durch große Requests
App ist normalerweise weit unter Limit, aber bei einem 100MB-Upload, einem Bulk-Import oder einer großen Query overshooted sie kurz und stirbt.
Diagnose: Peak liegt am Limit, durchschnittlicher Verbrauch nur bei 30-40%. Korreliert mit bestimmten Endpoints oder Cron-Jobs.
Fix: Streaming statt In-Memory-Buffering im Code, oder Pagination, oder einfach Limit groß genug für den Worst Case auslegen. Vorsicht: wenn der Worst Case 10x über dem Schnitt liegt, ist das Manifest falsch dimensioniert - dann lohnt sich ein eigener Worker-Deployment für die Heavy-Operations.
4. JVM, Node.js, Go ohne Runtime-Limit
Klassiker bei Java: ohne -Xmx nimmt sich die JVM 25% der Host-RAM - nicht des Container-Limits. Bei einem Node mit 64GB sind das 16GB Heap, in einem 1GB-Container ist das sofort OOMKill.
Kubernetes Debugging - systematisch statt raten
Echte Production-Incidents nachstellen, kubectl-Workflows verinnerlichen, Root-Causes in Minuten finden.
Workshop-Details ansehenDie richtigen Werte pro Runtime:
| Runtime | Setting | Beispiel für 1Gi Limit |
|---|---|---|
| Java 11+ | -XX:MaxRAMPercentage=75.0 | 768Mi Heap |
| Java (alt) | -Xmx<size> | -Xmx768m |
| Node.js | --max-old-space-size=<MB> | --max-old-space-size=768 |
| Go 1.19+ | GOMEMLIMIT (Env) | GOMEMLIMIT=900MiB |
| Python | resource.setrlimit(RLIMIT_AS, ...) | im Init-Code |
Faustregel: Runtime-Heap = Container-Limit × 0,75. Die restlichen 25% sind für Stack, Native, JIT, Metaspace, Threads.
5. Off-Heap- oder Native Memory
Heap-Dump zeigt 200MB Verbrauch, der Container ist bei 1GB. Das fehlende Memory liegt außerhalb des managed Heaps:
- JVM: Direct ByteBuffers (Netty, Kafka-Clients), Metaspace bei vielen Class-Loaders, JNI
- Node.js: Buffer-Allokationen außerhalb V8, native Addons (
sharp,node-canvas) - Python: numpy/pandas, jeder C-Extension-Code mit
malloc
Diagnose: Differenz zwischen kubectl top pod und Heap-Dump-Größe ist der Off-Heap-Verbrauch.
Fix: Native Memory Tracking aktivieren (-XX:NativeMemoryTracking=summary bei JVM), Off-Heap-Caches limitieren, oder eben einplanen und Limit anpassen.
6. Node-OOM (Pod ist nicht der Schuldige)
Pod-Limit ist 2GB, Pod verbraucht 800MB, trotzdem OOMKill. Was ist passiert?
Der Node hatte insgesamt zu wenig Memory. Kubelet markiert den Node als MemoryPressure und beginnt Pods zu evicten. Welcher Pod gekillt wird, hängt von der QoS-Klasse ab:
BestEffort(keine Requests/Limits) zuerstBurstable(Limits > Requests) als zweites - nach OOM-Score (verbrauchen sie mehr als ihren Request?)Guaranteed(Limit = Request) zuletzt
Diagnose: anderer Pod auf demselben Node hat ein Memory-Leak, oder Node ist generell überprovisioniert.
kubectl describe node <node> | grep -A 10 "Conditions"
kubectl get events -A --field-selector reason=Evicted
Fix: Garantierte QoS für kritische Workloads (limits == requests), priorityClassName: system-cluster-critical für Infrastruktur-Pods, und Node-Sizing am tatsächlichen Bedarf statt am Maximum.
Decision Tree
Reason ist wirklich OOMKilled (nicht nur Exit 137)?
↓ ja
Peak ≈ Limit?
↓ ja ↓ nein
Steigt Peak monoton über Tage? Andere Pods auf Node auch betroffen?
↓ ja ↓ nein ↓ ja → Ursache 6 (Node-Pressure)
Ursache 2 Korreliert mit ↓ nein
(Memory-Leak) bestimmten Reqs? Container ist JVM/Node/Go?
↓ ja ↓ nein ↓ ja → Ursache 4 (Heap-Limit fehlt)
U. 3 U. 1 oder Ursache 5 (Off-Heap)
(Spike) (zu klein)
Right-Sizing-Formel
Nach der Messung gilt:
Memory-Request = ⌈avg_usage⌉ # für Scheduler-Genauigkeit
Memory-Limit = ⌈peak_usage × 1,3⌉ # 30% Sicherheits-Puffer
Runtime-Heap = Memory-Limit × 0,75 # 25% für Stack, Native, JIT
Beispiel: Peak ist 700MB, Durchschnitt 400MB:
resources:
requests:
memory: "400Mi"
limits:
memory: "910Mi"
env:
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=75.0" # → 682Mi Heap
Bei sehr stabilen Workloads (Stateful-Sets, Datenbanken) macht Guaranteed QoS mehr Sinn:
resources:
requests:
memory: "1Gi"
limits:
memory: "1Gi" # Limit == Request → Guaranteed
Was VPA dazu beiträgt
Der Vertical Pod Autoscaler automatisiert Schritt 2-4. Er beobachtet den realen Verbrauch über Tage und schlägt Limits/Requests vor. Drei Modi:
Off- nur Empfehlungen, kein Eingriff (gute Startposition)Initial- setzt Werte beim Pod-Start, danach unverändertAuto- rekreiert Pods mit neuen Werten (nur für unkritische Workloads)
VPA ist kein Ersatz für die Cause-Analyse - bei Memory-Leaks (Ursache 2) verschiebt er den Crash nur. Aber für Ursache 1 und 3 ist er die richtige Antwort.
Was die Workshops nicht ersetzen
Dieser Workflow löst 80% aller OOMKill-Fälle. Was nicht in den 6 Ursachen steckt:
- OOMKill durch Sidecar-Container im selben Pod - Ressourcen werden auf Pod-Ebene aggregiert, ein Sidecar mit Memory-Leak killt den Hauptcontainer mit
- Eviction durch falsche
evictionHard-Schwellen am Kubelet - Pods sterben bei 90% Node-Memory, obwohl ihr eigenes Limit nicht voll ist - Kernel-Page-Cache zählt für
working_set- Container mit großenmmap-Dateien sehen viel höhere Verbrauchszahlen als ihr Heap
Diese Pattern brauchen System-Verständnis - genau das, was den Unterschied zwischen “Limit verdoppeln und hoffen” und “in 10 Minuten die Root Cause finden” ausmacht.
Wie es weitergeht
Im Kubernetes Debugging Workshop spielen wir 8 echte Production-Incidents nach - inklusive zweier OOMKill-Edge-Cases (JVM-Off-Heap und Node-Eviction unter Last) - und drillen den Workflow, bis er sitzt. 1 Tag, 8 Stunden, danach lösen Sie OOMKill systematisch und nicht durch Raten.
Verwandt aus unserer Debugging-Serie:
- CrashLoopBackOff systematisch fixen - 7 Ursachen, 1 Workflow für die zweithäufigste Pod-Krankheit
- kubectl Debugging Cheatsheet - die 12 Befehle, die jede Pod-Inspektion abdecken
- OOMKilled-Cheatsheet (interaktiv) - die kompletten Befehle dieses Posts mit Copy-Button, Runtime-Limits-Tabelle und Druck-Ansicht
- Pod Pending: 23 Ursachen, Decision-Tree - der dritte häufigste Pod-Zustand mit 5-Kategorien-Index und Cheatsheet
Kubernetes Debugging - systematisch statt raten
Echte Production-Incidents nachstellen, kubectl-Workflows verinnerlichen, Root-Causes in Minuten finden.
Workshop-Details ansehenWeiterlesen
CrashLoopBackOff systematisch fixen: 7 Ursachen, 1 Workflow
Die 7 häufigsten Ursachen für CrashLoopBackOff - mit kubectl-Befehlen, echten Outputs und einem Decision-Tree, der jede Ursache in unter 5 Minuten findet.
9 min
kubectl Debugging Cheatsheet: 12 Befehle für Production-Incidents
Strukturierter Debugging-Workflow für Kubernetes in Production: 12 kubectl-Befehle in der richtigen Reihenfolge - von Pod-Status bis ephemeral debug containers.
8 min
CrashLoopBackOff systematisch fixen: 7 Ursachen, 1 Workflow
Die 7 häufigsten Ursachen für CrashLoopBackOff - mit kubectl-Befehlen, echten Outputs und einem Decision-Tree, der jede Ursache in unter 5 Minuten findet.
9 minBrauchen Sie eine zweite Meinung zu Ihrem Cluster?
Buchen Sie einen kostenfreien 30-Minuten Kubernetes Health-Check. Wir schauen uns Ihr Setup an und geben konkrete Hinweise, ohne Verkaufsgespräch.
Termin buchen