Kubernetes · beginner
Kubernetes OOMKilled: correlate the limit with the process lifetime
Kubernetes terminates a container with OOMKilled when the kernel detects memory usage exceeding the configured cgroup limit. Correlate the exit reason, the container's memory limit, and the process's working-set growth over its lifetime to distinguish a legitimate spike from a misconfigured boundary or a leak.
The symptoms
- •Pod status shows a container with Last State Terminated and Reason OOMKilled in `kubectl describe pod`.
- •Container restartCount increases while the pod phase remains Running after a CrashLoopBackOff transition.
- •kubectl events for the pod report BackOff restarting failed container with a preceding OOMKilled reason.
- •Application metrics show a monotonic memory climb that crosses the limit just before the termination timestamp.
- •Steady-state working set sits well below the limit, but a specific operation (warm-up, cache fill, query) pushes it over the boundary.
- •Node-level dmesg or kubelet logs show cgroup memory throttling or kill events aligned with the container's lifecycle.
Likely causes
- •The memory limit in the pod spec is set below the process's actual working-set peak, so a normal workload operation crosses the cgroup boundary.
- •A memory leak grows resident set size over the container's lifetime until it finally exceeds a limit that was previously adequate.
- •A startup or warm-up phase (class loading, cache priming, JIT buffers) allocates more memory than the steady-state budget assumed.
- •The container is missing a memory limit, so it inherits a namespace quota or LimitRange that is tighter than the workload needs.
- •The metric being watched (container_memory_working_set_bytes) diverges from the cgroup memory limit accounting because of page cache or kernel overhead.
- •A sidecar or shared-memory volume consumes memory that counts against the same cgroup as the main process.
First ten minutes
- 01Confirm the termination reason with `kubectl describe pod <pod> -n <namespace>` and read the Last State Terminated Reason and Exit Code fields.
- 02List recent events for the pod with `kubectl get events --field-selector involvedObject.name=<pod> -n <namespace>` to see BackOff and OOMKilled events in order.
- 03Identify which container in the pod was killed; OOMKilled is reported per container, not per pod.
- 04Read the container's resource stanza from the pod spec to capture both resources.limits.memory and resources.requests.memory for the affected container.
- 05Compare the limit value against the container's peak working_set_bytes from the metrics endpoint over the same window the kill occurred.
- 06Check the pod's effective LimitRange and ResourceQuota in the namespace using `kubectl get limitrange,resourcequota -n <namespace> -o yaml` to rule out an inherited boundary.
Evidence to collect
- •Timestamp of the OOMKilled event from kubectl describe output and the corresponding container restart count.
- •The exact memory limit string (for example 512Mi) applied to the killed container at the time of termination.
- •container_memory_working_set_bytes and container_memory_rss samples from the 5 to 10 minutes preceding the kill.
- •Application-level heap or allocator stats for the same window, if the workload exposes them.
- •Node kubelet logs or cgroup memory.events file content showing the oom_kill counter increment for the container's cgroup path.
- •Whether any sidecar containers share the same pod cgroup and their individual memory usage at kill time.
Where to look
- •The cgroup v2 path /sys/fs/cgroup/.../<container-id>/memory.current and memory.events for the killed container on the node that hosted the pod.
- •The pod spec field spec.containers[].resources.limits.memory and spec.containers[].resources.requests.memory.
- •The namespace's LimitRange object, which can set default memory limits that override absent values in the pod spec.
- •The kubelet log on the node (typically /var/log/kubelet.log or journalctl -u kubelet) for memory pressure or eviction entries near the kill time.
- •The metrics endpoint scraped by Prometheus or the cluster's metrics-server for container_memory_working_set_bytes labeled with the pod and container.
- •kubectl describe output sections State and Conditions, plus Events at the bottom, for the canonical Reason string.
Diagnostic steps
- 01If the Reason is OOMKilled and Exit Code is 137, attribute the termination to a cgroup memory kill rather than an application crash; proceed to memory correlation.
- 02If Exit Code is 139 or 134 instead, suspect a native segfault or runtime abort and treat the OOMKilled label as misleading; inspect core dumps and runtime logs first.
- 03Compute the ratio of peak working_set_bytes to the configured memory limit over the kill window; values consistently above 0.9 indicate the limit is the binding constraint.
- 04Plot or sample working_set_bytes across the container's full lifetime, not just the last minute, to distinguish a steady leak from a one-shot spike tied to a specific request or job.
- 05Compare the lifetime curve to the application's expected memory profile (constant, bounded cache, JVM heap, native buffers) to decide whether the growth is normal or pathological.
- 06Check whether a LimitRange in the namespace is silently capping the limit you set; if so, the effective limit in describe output will differ from your manifest.
- 07Verify whether multiple containers in the pod share a single cgroup (default behavior in cgroup v2 with Kubernetes) and whether sidecar memory counts against the main container's budget.
Common mistakes
- •Treating OOMKilled as an application bug and increasing the limit without checking working_set_bytes, which masks leaks and inflates node pressure.
- •Reading container_memory_usage_bytes instead of container_memory_working_set_bytes and concluding headroom exists when page cache is hiding real growth.
- •Assuming requests and limits are independent: exceeding the limit causes the kill even if requests were respected for scheduling.
- •Ignoring a namespace LimitRange that clamps the requested limit to a lower default, leading to repeated kills after a manifest change that appeared to take effect.
- •Restarting the pod as a remediation step without capturing pre-kill memory samples, which destroys the evidence needed to size a correct limit.
- •Conflating node-level memory pressure eviction (Reason Evicted) with container-level OOMKilled; the remediation paths differ.
Safe fixes
- •If working_set_bytes peaks just above the limit during a known warm-up, raise the limit only to a value documented in the workload's memory budget, and keep requests at or below the previous limit to preserve scheduling behavior.
- •If working_set_bytes grows monotonically across the lifetime with no plateau, treat it as a leak and roll back or patch before adjusting limits; do not raise the limit past the node's allocatable memory.
- •If a LimitRange is clamping the limit, either update the LimitRange to a value consistent with workload needs or add an explicit limit in the pod spec that exceeds the LimitRange default.
- •For JVM or similar runtimes, align the in-process heap ceiling (such as -Xmx) to leave headroom under the cgroup limit for non-heap, metaspace, threads, and native overhead.
- •For multi-container pods, sum the expected peak usage of every container in the pod and set the per-container limits so the pod total stays within node capacity and any applicable quota.
- •After any change, redeploy and observe at least one full lifetime cycle of working_set_bytes before declaring the fix stable.
Prove the fix
- 01kubectl describe pod <pod> -n <namespace> shows no new OOMKilled entry in Last State across at least one full lifetime cycle after the change.
- 02container_memory_working_set_bytes stays below the new limit with a documented safety margin (commonly below 80 percent of the limit) across the same window.
- 03container_restart_count for the affected container does not increment during the observation window.
- 04A synthetic load or replay that previously reproduced the kill completes without producing a new BackOff or OOMKilled event.
- 05If a leak was suspected, working_set_bytes plateaus rather than trending upward across successive restarts with comparable load.
Prevention and next steps
- •Define a memory budget per workload that states steady-state working set, expected peak during warm-up, and the cgroup limit, and store it alongside the manifest.
- •Add an alert on container_memory_working_set_bytes approaching the limit (for example at 80 percent sustained) so growth is detected before a kill.
- •Track container restart count and OOMKilled event rate as SLO signals so regressions surface in dashboards rather than as incidents.
- •Review namespace LimitRange and ResourceQuota whenever a new workload is onboarded so default limits match the expected memory profile.
- •Run periodic chaos or load rehearsals that push the workload past steady state in a staging environment to validate headroom assumptions.
Safe commands and checks
kubectl describe pod <pod> -n <namespace>
kubectl get events --field-selector involvedObject.name=<pod> -n <namespace> --sort-by=.lastTimestamp
kubectl get pod <pod> -n <namespace> -o jsonpath='{.spec.containers[*].resources.limits.memory}{"\n"}'
kubectl get limitrange -n <namespace> -o yaml
kubectl get resourcequota -n <namespace> -o yaml
cat /sys/fs/cgroup/.../<container-id>/memory.events
kubectl logs <pod> -n <namespace> --previous --tail=200