Cheveo Blog
debugging 10 min Lesezeit ·

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.

Clemens Christen
Clemens Christen Certified Kubernetes Administrator (CKA)

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:

  1. Container alloziert Memory bis er sein cgroup-Memory-Limit erreicht
  2. Kernel sendet SIGKILL an den Container-Hauptprozess
  3. Container-Runtime meldet Exit-Code 137 (128 + Signal 9)
  4. Kubelet schreibt Reason: OOMKilled in den Pod-Status
  5. 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.

1-Tag Intensiv-Workshop

Kubernetes Debugging - systematisch statt raten

Echte Production-Incidents nachstellen, kubectl-Workflows verinnerlichen, Root-Causes in Minuten finden.

Workshop-Details ansehen

Die richtigen Werte pro Runtime:

RuntimeSettingBeispiel für 1Gi Limit
Java 11+-XX:MaxRAMPercentage=75.0768Mi 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
Pythonresource.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:

  1. BestEffort (keine Requests/Limits) zuerst
  2. Burstable (Limits > Requests) als zweites - nach OOM-Score (verbrauchen sie mehr als ihren Request?)
  3. 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ändert
  • Auto - 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ßen mmap-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:

1-Tag Intensiv-Workshop

Kubernetes Debugging - systematisch statt raten

Echte Production-Incidents nachstellen, kubectl-Workflows verinnerlichen, Root-Causes in Minuten finden.

Workshop-Details ansehen
Kostenfrei · 30 Minuten

Brauchen 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