Kubernetes Workload Failures: CrashLoopBackOff, ImagePullBackOff, Pending and OOMKilled
Four pod states cause most of the pain in Kubernetes. This guide gives you a repeatable diagnosis workflow for each, with the commands to run and the log lines that tell you the real cause.
Start with the pod, not the cluster
Every pod failure investigation starts the same way: get the pod's own account of events before touching nodes or networking.
kubectl get pod <name> -n <ns> -o wide kubectl describe pod <name> -n <ns> # Events section is the gold kubectl logs <name> -n <ns> --previous # the crashed attempt, not the waiting one
describe shows scheduling and pull events; logs --previous shows why the process died. Ninety percent of cases end here.
CrashLoopBackOff: the process starts, then dies
The container ran and exited (or was killed) repeatedly, and kubelet is now backing off restarts. The exit code in describe tells you which family of problem you have:
| Last State / exit code | Usual cause |
|---|---|
Exit Code 1 | Application error: bad config, missing env var, failed migration, unhandled exception |
Exit Code 137 (SIGKILL) | OOMKilled by the cgroup, or a liveness probe killing the process |
Exit Code 139 (SIGSEGV) | Segmentation fault — often wrong binary/architecture (amd64 image on arm64 node) |
Exit Code 0 in a loop | The container "succeeds" and stops — a long-running app packaged as a one-shot command |
kubectl logs <pod> -n <ns> --previous --tail=100
kubectl get events -n <ns> --sort-by=.lastTimestamp | tail
kubectl get pod <pod> -n <ns> -o jsonpath='{.status.containerStatuses[0].lastState}'Typical culprits in the logs: connection refused to a database that is not ready (add an initContainer or startup probe), ConfigMap "x" not found, or a missing secret key that crashes the app at boot.
ImagePullBackOff: the image never arrives
The event text distinguishes the four causes instantly:
| Event text | Cause and fix |
|---|---|
manifest not found / not found | Wrong tag or repository name. Verify with crane manifest or the registry UI |
unauthorized / 401 | Missing or wrong imagePullSecret; the ServiceAccount has no secret attached |
x509: certificate signed by unknown authority | Private registry with an internal CA — add the CA to the node's trust store or mark the registry insecure at the container runtime |
context deadline exceeded / timeouts | Node cannot reach the registry: egress firewall, proxy, or DNS |
# on the node where the pod is scheduled crictl pull <image> # containerd systemctl status containerd curl -sI https://registry.example.com/v2/
If crictl pull fails with a TLS error but curl works, the runtime's trust store is the problem, not the network.
Pending pods: nothing is wrong with the pod, something is wrong with capacity
The Events section will say exactly why. The common ones:
0/5 nodes are available: 3 node(s) had taint {...}, 2 node(s) didn't match Pod's node affinity— taints/tolerations or affinity rules exclude every node.Insufficient cpu/Insufficient memory— requests exceed what is schedulable. Compare withkubectl describe node→ Allocated resources.pod has unbound immediate PersistentVolumeClaims— the PVC is unbound; the StorageClass may not exist or the provisioner is failing. Checkkubectl get pvcand the provisioner's logs.
A node with 4 vCPU does not offer 4 vCPU to pods; kubelet and system daemons reserve part of it. If requests across the namespace total near the allocatable value, the next pod goes Pending even though top shows the node idle — scheduling uses requests, not usage.
kubectl describe node <node> | sed -n '/Allocated resources/,/Events/p'
OOMKilled: the kernel or kubelet killed the container
Exit Code 137 with Reason: OOMKilled in lastState means the cgroup limit was hit — the container exceeded its own resources.limits.memory. A node-level OOM (kernel oom-killer under system pressure) shows in dmesg on the node and may kill any process, not just the limited one.
kubectl get pod <pod> -o jsonpath='{.status.containerStatuses[*].lastState.terminated}'
# on the node:
dmesg -T | grep -i 'out of memory'- Check real usage over a week:
kubectl top podnow, but use metrics-server history or Prometheuscontainer_memory_working_set_bytesfor the p99. - Set
requestsnear observed p50 andlimitsnear p99 + headroom. Working-set spikes during GC or large batch reads are the usual killers. - Java workloads: the JVM heap is not the whole story — set
-XX:MaxRAMPercentageand leave 25–30% for metaspace, threads and off-heap.
Probes: when Kubernetes kills a healthy app
A container restarting with Exit Code 137 but no OOMKilled reason is usually the kubelet enforcing a failing liveness probe. Check describe for Liveness probe failed: HTTP probe failed with statuscode 503. Fixes: give slow-starting apps a startupProbe with generous failureThreshold, point liveness at a cheap endpoint (not one that checks the database), and never let liveness and readiness share an expensive handler.
Prevention checklist
- Ship logs to a collector —
kubectl logs --previousis lost after the next restart. - Set requests and limits on every workload; unbounded pods are the root of both Pending and node OOM drama.
- Use
startupProbefor anything that takes over 10 seconds to boot. - Tag images immutably (SHA or semver), never
latestin production —ImagePullBackOffinvestigations double when tags move. - Alert on
kube_pod_status_phase{phase="Pending"}older than 5 minutes and onkube_pod_container_status_restarts_totalrate.