CrashLoopBackOff tells you exactly one thing: a container started, exited, and Kubernetes is now waiting an increasing amount of time before trying again. It does not tell you why. Treating it as if it were the diagnosis — restarting the pod, bumping replicas, redeploying the same manifest — is how a five-minute problem turns into an hour of guessing.
The causes fall into a small number of distinct categories, and each one leaves different evidence. The fastest path to a fix is identifying which category you're in before changing anything.
Short answer
kubectl get pods -n <namespace>
kubectl describe pod <pod> -n <namespace>
kubectl logs <pod> -n <namespace> --previous
describe pod is doing most of the work here — the Last State block tells you the exit reason (OOMKilled vs. a plain exit code), and the Events section at the bottom shows probe failures, image pull errors, and scheduling issues in roughly chronological order. --previous gets you the logs from the container that just crashed, not the new one that's about to crash again — this is the single most-forgotten flag in this whole workflow, and without it you're often looking at empty or misleadingly fresh logs.
From there, the cause is almost always one of:
- The application is crashing on its own (bad config, unhandled exception, wrong startup args)
- It got OOMKilled
- A liveness probe is killing an otherwise-healthy container
- It's missing a ConfigMap, Secret, or environment variable it needs at startup
- It's trying to reach a dependency that isn't ready yet
Each has a distinct signature in describe pod and the logs, covered below.
Reading the pod status correctly
kubectl get pods -n checkout
NAME READY STATUS RESTARTS AGE
checkout-api-7d9f8c5b6-k2p9x 0/1 CrashLoopBackOff 7 (98s ago) 12m
Two numbers matter before you look at anything else: RESTARTS and the backoff interval implied by AGE vs. the "ago" timestamp. A high restart count with a short, consistent interval usually means an immediate startup failure (the container never gets far before exiting). A restart count that took a while to climb, with growing gaps between attempts, points at something time-dependent — a liveness probe grace period, or a dependency the container waits on before failing.
Kubernetes' backoff is exponential and capped at five minutes between attempts (documented behavior), so if you're seeing multi-minute gaps between restarts, that's expected mechanics, not evidence of anything specific on its own — look at the actual exit reason instead.
Cause 1: the application is exiting on its own
kubectl logs checkout-api-7d9f8c5b6-k2p9x -n checkout --previous
If the logs show a stack trace, an unhandled exception, or an explicit "cannot connect to X, exiting" message, the application told you exactly what's wrong — read it before assuming it's a Kubernetes problem. Common variants here: a missing required environment variable causing an immediate config-validation failure, a database migration that hasn't run yet, or a startup dependency check that's stricter than the actual runtime requirement.
Check the exit code too, from describe pod:
Last State: Terminated
Reason: Error
Exit Code: 1
Started: Mon, 22 Sep 2026 09:14:02 +0000
Finished: Mon, 22 Sep 2026 09:14:03 +0000
Exit Code: 1 is generic — the application decided to exit, and the reason is in the logs. Exit Code: 137 means the container was killed by SIGKILL (128 + 9), which usually means an OOM kill or an external termination, not a graceful application exit — that routes you to the next section instead of back to application logs.
Cause 2: OOMKilled
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
This is unambiguous once you see it in describe pod — the kernel's OOM killer terminated the container because it exceeded its memory limit. The fix isn't "raise the limit and move on" without understanding why: check whether the workload has a genuine memory requirement above what's set, or whether there's a leak that means it'll hit any limit eventually, just later.
kubectl top pod checkout-api-7d9f8c5b6-k2p9x -n checkout
gives current usage, but for a crash-looping pod that's often not useful — it restarted before you could observe the climb. If this happens repeatedly, watch usage over the container's actual lifetime instead of a single snapshot, or check whatever metrics backend already scrapes container_memory_working_set_bytes for a memory trend leading up to each kill.
This is entirely a resource-configuration and application-behavior question, covered in depth — including QoS classes, how requests/limits interact with scheduling, and how to size them from real usage — in getting Kubernetes resource requests and limits right in production.
Cause 3: a failing liveness probe
This is the cause that's easy to misdiagnose, because the container logs often look completely normal — the application is healthy, and Kubernetes is killing it anyway.
Events:
Warning Unhealthy 2m (x4 over 4m) kubelet Liveness probe failed: HTTP probe failed with statuscode: 503
Normal Killing 2m kubelet Container checkout-api failed liveness probe, will be restarted
The Events section is where this shows up, not the container logs — the kubelet is killing the container from the outside, so there's often nothing in the application's own log output explaining it.
The distinction worth being explicit about: a failing readiness probe does not cause CrashLoopBackOff. A readiness probe failure removes the pod from Service endpoints (it stops receiving traffic) but leaves the container running. Only a liveness probe failure gets the container killed and restarted. If you're seeing CrashLoopBackOff and a probe is involved, it's the liveness probe, not readiness — conflating the two sends you looking in the wrong Service/Endpoints direction instead of at the probe configuration itself.
The most common cause of a liveness probe wrongly killing a healthy container: initialDelaySeconds set shorter than the application's actual startup time, so the probe starts checking (and failing) before the app is ready to serve it. This is exactly what a startupProbe exists to fix — it holds off the liveness probe until the startup probe first succeeds, which decouples "how long could startup reasonably take" from "how fast should we detect a hung process once running":
# Illustrative — decouples startup time from liveness sensitivity.
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10
failureThreshold: 3
With this, the app has up to 150 seconds (30 × 5s) to pass its first health check before the liveness probe is evaluated at all, and once running, a genuine hang is caught within 30 seconds (3 × 10s) instead of the slow, generous startup window.
Cause 4: missing ConfigMap, Secret, or environment variable
If the pod never gets to Running at all and sits in CreateContainerConfigError before you even see CrashLoopBackOff, that's a different failure — a referenced ConfigMap or Secret key doesn't exist. But if the container does start and then exits immediately, a missing optional-looking environment variable that the application actually requires at startup produces the same signature as cause 1 (application exits on its own) — check the logs for a config-validation error naming the specific missing value before assuming it's something more complex.
kubectl get pod checkout-api-7d9f8c5b6-k2p9x -n checkout -o jsonpath='{.spec.containers[0].envFrom}'
kubectl get configmap checkout-api-config -n checkout -o yaml
confirms whether the ConfigMap/Secret the pod references actually exists and has the keys the container expects.
Cause 5: a dependency isn't ready yet
Logs show the application trying (and failing) to connect to a database, message queue, or another service, then exiting instead of retrying. This is architecturally different from the previous causes — the fix isn't in this pod's config, it's in how the pod handles a not-yet-ready dependency at all.
Two acceptable fixes, and one that isn't:
- The application retries with backoff instead of exiting. This is the actual fix for most cases — a service that depends on another service should tolerate that dependency being briefly unavailable, especially during a rolling deploy or cluster event.
- An init container blocks pod startup until the dependency is reachable, if the application genuinely can't be made to retry (e.g., you don't control its code):
# Illustrative — blocks the main container until the dependency accepts connections.
initContainers:
- name: wait-for-postgres
image: busybox:1.36
command: ['sh', '-c', 'until nc -z postgres.data.svc.cluster.local 5432; do sleep 2; done']
- Adding
sleep 30before the app starts is not a fix — it's a race condition with a slightly longer fuse. It works until deploy timing, dependency startup time, or cluster load shifts by more than 30 seconds, and then you're back here.
If the "dependency" is actually DNS resolution failing rather than the dependency itself being down, that's a distinct diagnosis with its own systematic process — see debugging DNS resolution failures in Kubernetes rather than assuming the downstream service is at fault.
Common mistakes
- Restarting or redeploying before reading
describe pod. It destroys theLast Stateblock you need — a fresh pod created by a rollout has no crash history yet. - Forgetting
--previousonkubectl logsand concluding "there are no logs" when the crashing container's logs simply aren't the ones being shown. - Treating every probe failure as a liveness issue. Check
Eventsfor which probe actually failed — readiness failures explain a pod stuck out of a Service's endpoints, not a restart. - Raising a liveness probe's
failureThresholdortimeoutSecondsas a blanket fix without adding astartupProbe, which just makes genuine hangs take longer to detect instead of fixing the startup-timing mismatch that caused the false failures. - Assuming exit code 137 is always OOM. It's
SIGKILL, which is what an OOM kill produces, but akubectl delete pod --grace-period=0 --forceor a node-level issue can produce the same code — confirm theReason: OOMKilledfield specifically rather than inferring it from the exit code alone.
Prevention
- Set
startupProbealongsidelivenessProbefor any application with a startup time that isn't near-instant — this alone eliminates a large share of "healthy container, false liveness kill" incidents. - Size resource limits from observed usage, not guesses, and revisit them after a genuine architecture or traffic change — see the resource requests and limits guide for how.
- Make dependency handling the application's responsibility (retry with backoff) rather than the deployment's responsibility (hope the timing works out).
- Fail loudly and specifically on missing config at startup — a clear "missing required env var DATABASE_URL" log line turns a 20-minute investigation into a 20-second one.
Conclusion
CrashLoopBackOff is a symptom with five common, distinguishable causes — read describe pod's Last State and Events sections and the --previous logs before changing anything, and the actual cause is usually obvious within a couple of minutes. The exit code and the Reason field alone separate an application crash from an OOM kill from a probe-driven restart; the rest is reading what's already there.
If this kind of investigation is a recurring pattern rather than a one-off — probes misconfigured across several services, resource limits nobody's revisited, dependency handling that assumes everything starts in order — that's usually worth a broader look at how the cluster's workloads are configured. That's the kind of reliability and troubleshooting work covered under Kubernetes Consulting, and a good starting reference for what "configured correctly" looks like across a whole cluster is the Kubernetes Production Readiness Guide.