you've probably seen a pod go into CrashLoopBackOff after a sudden traffic spike, even though the container's memory request matches the node's capacity. the root cause is an invisible cgroup pressure that the scheduler can't see, and a missing hard limit that lets the process eat the node's RAM until the kernel OOM killer steps in.
the hidden pressure
kubernetes only schedules based on the request field; if you omit a hard limit, the container can exceed its request without any back‑pressure. the node's cgroup will then balloon, starving sibling pods and eventually triggering a node‑wide OOM. the failure surface is hard to diagnose because the pod logs show nothing unusual—only the termination reason "OOMKilled".
apiVersion: v1
kind: Pod
metadata:
name: memory‑stress
spec:
containers:
- name: app
image: myorg/app:latest
resources:
requests:
memory: "256Mi"
limits:
memory: "512Mi" # explicit hard limit
env:
- name: ENABLE_METRICS
value: "true"
- name: cgroup‑exporter
image: prom/cgroup-exporter:latest
resources:
limits:
memory: "64Mi"
volumeMounts:
- name: cgroup
mountPath: /host/sys/fs/cgroup
volumes:
- name: cgroup
hostPath:
path: /sys/fs/cgroup
the sidecar above scrapes /sys/fs/cgroup and pushes memory usage into Prometheus, giving you a real‑time view of the container's actual consumption versus its limits. coupling this with an alert on container_memory_working_set_bytes approaching container_spec_memory_limit_bytes catches overshoots before the kernel kills the pod.
trade‑offs and failure modes
- limit too low – the pod throttles under normal load, increasing latency and causing request retries that amplify load. you need to benchmark realistic burst patterns and set the limit a safe headroom (often 1.5‑2× the 95th‑percentile usage).
-
limit too high – you re‑introduce noisy‑neighbor risk; a memory leak in one tenant can starve the rest of the node. run chaos experiments that inject a synthetic leak (
stress --vm 2 --vm-bytes 300M) to verify bulkhead isolation. - metrics lag – if the exporter polls too infrequently, you may react after the OOM event. configure a scrape interval of ≤15s for critical services.
-
node‑level cgroup fragmentation – on long‑running clusters, leftover cgroup hierarchies can corrupt accounting. schedule a periodic
systemd-run --quiet --property=Delegate=yescleanup job to prune orphaned groups.
by treating the memory limit as a contract rather than a suggestion, you turn an invisible failure domain into an observable one. the contract must be continuously validated in production; otherwise you inherit the same brittle behavior you tried to avoid.
what's your approach to validating pod memory limits in production without triggering OOM kills
Top comments (2)
One correction worth making: a memory limit that is too low does not throttle the pod. Memory is not a throttleable resource the way CPU is, so when a container crosses its memory limit the kernel OOM killer ends it and Kubernetes restarts the container. A too-low memory limit shows up as OOMKilled restarts, not latency.
For your question about validating without killing production, I would run VPA in recommendation mode for a week to get a baseline, then set the limit a bit above the p99 working set and prove it in staging under a load test before touching prod.
The 'schedules on requests' trap is the one most teams walk into — requests feel like a reservation, but without a hard limit the container borrows the whole node until the kernel collects. I've seen the exact OOMKilled-with-silent-logs symptom after a traffic spike, and the fix was the same pairing: hard limit plus a working-set alert before it. The retry-amplification feedback loop is honestly named — limits too low cause retries, retries cause memory pressure, pressure causes more OOMs. On your closing question: I validate with a canary carrying the candidate limit and watch working-set p99, not the average — averages hide the spike that kills you. Are you sizing from p99, or request-plus-headroom?