CrashLoopBackOff is not a cause. It is Kubernetes telling you that a container started, exited, and has been restarted several times with an increasing delay between attempts — 10s, 20s, 40s, doubling up to a five-minute cap.
The actual cause is somewhere else, and there are six realistic candidates.
Get the real error first
Do these two things before anything else. Between them they identify the cause in the large majority of cases.
# 1. The logs of the run that CRASHED — not the one starting now
kubectl logs payments-api-7d4f-x8k2 --previous
--previous is the whole trick. Plain kubectl logs shows the container that has just started, which during a backoff has produced nothing yet. That is why people report "CrashLoopBackOff with empty logs" — they are reading the wrong instance.
# 2. Events and the termination reason
kubectl describe pod payments-api-7d4f-x8k2
The bottom of that output is what matters:
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Events:
Warning BackOff kubelet Back-off restarting failed container
And the single most useful one-liner:
kubectl get pod payments-api-7d4f-x8k2 \
-o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}{"\n"}{.status.containerStatuses[0].lastState.terminated.exitCode}'
Match the exit code to the cause
| Exit code | Reason | What it means |
|---|---|---|
137 | OOMKilled | Exceeded the memory limit — cause 1 |
137 | Error | SIGKILL from a failed liveness probe — cause 4 |
143 | Error | SIGTERM — something asked it to stop |
1 | Error | Generic application failure — cause 2 or 3 |
0 | Completed | Finished successfully — cause 5 |
127 | Error | Command not found — cause 6 |
126 | Error | Command found but not executable |
Cause 1 — OOMKilled
The most common by a distance. The container exceeded its memory limit and the kernel killed it instantly, so the application never got to log anything.
kubectl get pod <pod> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'
# OOMKilled
Check what it actually uses, then raise the limit — or find the leak:
kubectl top pod <pod> --containers
A JVM, Node or .NET application is a special case. These runtimes size their heap against what they believe is available memory, and without being told about the container limit they will happily size against the node's total. A container limited to 512Mi with a JVM sizing a 16GB heap is OOMKilled reliably.
env:
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=75"
Cause 2 — Missing configuration
The application starts, cannot find a required environment variable or config file, and exits. The logs usually say so plainly, which is why --previous matters.
kubectl logs <pod> --previous
# Error: DATABASE_URL is not defined
Check what the container actually received:
kubectl exec <pod> -- env | sort # if it stays up long enough
kubectl get pod <pod> -o jsonpath='{.spec.containers[0].env}'
A related failure looks different: if a referenced ConfigMap or Secret does not exist, the Pod never starts at all and shows CreateContainerConfigError rather than CrashLoopBackOff.
Cause 3 — A dependency is unreachable at startup
The application tries to connect to a database, cache or API during boot, fails, and exits rather than retrying.
kubectl logs <pod> --previous
# Error: connect ECONNREFUSED 10.96.41.203:5432
Test reachability from inside the cluster:
kubectl run tmp --rm -it --image=nicolaka/netshoot -- \
nc -zv payments-db.payments.svc.cluster.local 5432
The usual causes are a wrong Service name, a missing namespace segment in the DNS name, or a NetworkPolicy blocking the traffic.
The durable fix is for the application to retry with backoff rather than exit. A dependency being briefly unavailable is normal; crashing because of it converts a transient blip into a crash loop.
Cause 4 — A liveness probe killing a slow starter
The application takes 60 seconds to boot, the liveness probe starts checking after 10, fails three times, and the kubelet kills it. It restarts and the cycle repeats — a crash loop caused entirely by the health check.
Events:
Warning Unhealthy Liveness probe failed: Get "http://10.244.1.17:8080/healthz":
dial tcp: connect: connection refused
Normal Killing Container api failed liveness probe, will be restarted
The giveaway is Killing in the events with clean application logs — the app was fine, it was just not ready yet.
The fix is a startup probe, not a longer initialDelaySeconds:
startupProbe:
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 10
failureThreshold: 30 # up to 300s to boot
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 10
failureThreshold: 3 # then detect a hang in ~30s
A startup probe suspends liveness until it passes, so you get a generous boot window and fast detection afterwards. Inflating initialDelaySeconds gives you the first at the cost of the second.
Cause 5 — The process completes and exits
Exit code 0 with Reason: Completed. The container did its job and stopped — but restartPolicy: Always, which Deployments use, restarts a container whatever its exit code.
This means you have deployed batch work as a long-running service. Use a Job or CronJob instead, which set restartPolicy: OnFailure or Never and understand completion.
It also happens when a server daemonises itself into the background, making PID 1 exit. Keep the process in the foreground.
Cause 6 — Wrong command or entrypoint
Exit code 127 means command not found; 126 means found but not executable.
kubectl logs <pod> --previous
# exec /app/start.sh: no such file or directory
Usually a typo in command, a script missing the executable bit, or a shebang mismatch. A frequent variant: a script built on Windows with CRLF line endings fails with no such file or directory pointing at the interpreter, which is baffling until you know.
Check what the image actually defines:
kubectl get pod <pod> -o jsonpath='{.spec.containers[0].command} {.spec.containers[0].args}'
docker inspect --format '{{.Config.Entrypoint}} {{.Config.Cmd}}' <image>
Debugging when the container dies too fast
Sometimes it exits before you can inspect anything. Two approaches.
Override the command so it stays up, then poke around inside:
kubectl run debug --image=<your-image> --restart=Never -it --command -- sh
Attach an ephemeral container to the failing Pod, sharing its namespaces:
kubectl debug -it <pod> --image=nicolaka/netshoot --target=<container>
And to stop the backoff delay slowing you down while iterating, delete the Pod so a fresh one starts immediately rather than waiting out the timer.
A checklist
kubectl logs <pod> --previous— the crashed run's output.kubectl describe pod <pod>— events andLast State.- Read the exit code against the table above.
137+OOMKilled→ raise the memory limit, or fix the runtime's heap sizing.Killingin events with clean logs → add a startup probe.- Exit
0→ it should be a Job, not a Deployment. - Exit
127→ wrong command, missing file, or CRLF line endings.
Frequently Asked Questions
What does CrashLoopBackOff actually mean?
It means a container has started, exited, and been restarted repeatedly, and the kubelet is now waiting before trying again — 10 seconds, then 20, then 40, doubling up to a five-minute cap. The "BackOff" part is the delay, not the failure. It is a symptom rather than a diagnosis: the real cause is whatever made the container exit, which you find in the logs of the previous run and the Pod's events.
Why does kubectl logs show nothing for a CrashLoopBackOff pod?
Because it reads the container that is running now, and during a backoff that instance has either not started or has only just started and produced no output. The output you need belongs to the terminated instance, which kubectl logs <pod> --previous retrieves. This is the single most common stumbling block with crash loops, and it accounts for most "empty logs" reports.
How do I know if CrashLoopBackOff is caused by out of memory?
Check the termination reason: kubectl get pod <pod> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'. If it returns OOMKilled, the container exceeded its memory limit and the kernel killed it — which is why the application logged nothing before dying. Compare actual usage with kubectl top pod before simply raising the limit, since unbounded growth is a leak that a bigger limit only postpones.
Why does my container crash loop even though the application is fine?
Most often a liveness probe killing it during startup. If the application needs 60 seconds to boot and the probe begins checking at 10, it fails, the kubelet kills the container, and the cycle repeats. The signature is Killing in the Pod events with clean application logs. Add a startup probe with a generous failureThreshold, which suspends the liveness probe until the application is up, rather than increasing initialDelaySeconds and losing fast failure detection later.
What does exit code 137 mean in Kubernetes?
It is 128 plus 9, meaning the process received SIGKILL. The two realistic sources are the kernel's OOM killer when a memory limit is exceeded, and the kubelet terminating a container that failed its liveness probe. lastState.terminated.reason distinguishes them: OOMKilled for the first, Error with a Killing event for the second. Exit 143 is 128 plus 15, SIGTERM, which usually indicates an ordinary shutdown rather than a fault.
How do I stop a pod crash looping while I debug it?
Scale the Deployment to zero with kubectl scale deployment/<name> --replicas=0 to stop the churn, then investigate using a separate debug Pod: kubectl run debug --image=<your-image> --restart=Never -it --command -- sh starts the same image with a shell instead of the failing command, so you can inspect the filesystem, environment and configuration directly. For a Pod that is still cycling, kubectl debug attaches an ephemeral container without disturbing it.