A pod in CrashLoopBackOff has a rising restart count:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
api-6b8f7d9c5d-h4trm 0/1 CrashLoopBackOff 5 (48s ago) 4m
CrashLoopBackOff is a symptom, never a cause. Your container started, exited, and Kubernetes restarted it — and after each failure it waits longer before trying again, doubling the delay up to a five-minute cap. The status simply reports that it is currently in one of those waiting periods.
Getting the actual cause takes one command, and the flag is the important part:
kubectl logs api-6b8f7d9c5d-h4trm --previous
--previous returns the logs of the run that crashed. Without it you get the current attempt, which is usually either still starting or already gone — which is why "there are no logs" is such a common and misleading first impression.
Step 1: Get the Exit Code
The exit code narrows the cause faster than anything else:
kubectl describe pod api-6b8f7d9c5d-h4trm
Look for Last State:
Last State: Terminated
Reason: Error
Exit Code: 1
Started: Tue, 12 Aug 2026 14:02:11 +0000
Finished: Tue, 12 Aug 2026 14:02:12 +0000
| Exit code | Meaning | Where to look |
|---|---|---|
0 | Exited successfully — the process finished its work | The container has nothing long-running to do |
1 | Generic application error | kubectl logs --previous |
126 | Command found but not executable | Entrypoint permissions in the image |
127 | Command not found | Wrong command/args, or missing binary in the image |
137 | SIGKILL (128 + 9) — usually the OOM killer | Memory limits |
139 | SIGSEGV (128 + 11) — segmentation fault | Application or native dependency bug |
143 | SIGTERM (128 + 15) — asked to stop and did | Probes, evictions, or a normal shutdown |
Also check Reason on the same block. OOMKilled is definitive; Error is generic.
Cause 1: The Application Fails at Startup (exit 1)
The overwhelming majority of cases. The process started, tried to initialise, and gave up:
kubectl logs api-6b8f7d9c5d-h4trm --previous
Typical output:
Error: connect ECONNREFUSED 10.96.0.42:5432
at TCPConnectWrap.afterConnect [as oncomplete]
Common versions of this:
- A missing environment variable, often because a ConfigMap or Secret key is misspelled. A key that does not exist is injected as empty rather than erroring, so the application fails later with a confusing message.
- A dependency that is not up yet. Databases and caches routinely start after the application that needs them.
- A config file that is not where the app expects it, usually a volume mounted at the wrong path.
Check what the container actually received:
kubectl set env deployment/api --list
kubectl get configmap api-config -o yaml
For a dependency that is merely slow, the right fix is retry-with-backoff in the application, or an init container that waits:
initContainers:
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c', 'until nc -z db 5432; do echo waiting for db; sleep 2; done']
An init container is better than raising the restart tolerance, because the main container never starts in a known-bad state.
Cause 2: Out of Memory (exit 137, OOMKilled)
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
The container exceeded its memory limit and the kernel killed it. This is not a soft warning — the process gets SIGKILL and cannot clean up.
Check what is configured:
kubectl get pod api-6b8f7d9c5d-h4trm -o jsonpath='{.spec.containers[*].resources}'
Then either raise the limit or reduce consumption:
resources:
requests:
memory: "256Mi"
limits:
memory: "512Mi"
Two subtleties worth knowing. Raising the limit only helps if the application has a genuine steady-state need — with a leak it delays the kill rather than preventing it, and the interval between restarts is the diagnostic. And JVM or Node applications need to be told about the limit; a JVM that does not see the cgroup limit sizes its heap against the node's total memory and is killed long before it thinks it is under pressure.
Cause 3: A Failing Liveness Probe (exit 137 or 143)
This one is easy to miss because the application is often perfectly healthy. If the liveness probe fails, the kubelet kills the container regardless.
The tell is in the events:
Warning Unhealthy 30s (x3 over 50s) kubelet Liveness probe failed: Get "http://10.244.1.5:8080/healthz": context deadline exceeded
Normal Killing 30s kubelet Container api failed liveness probe, will be restarted
The usual mistake is a liveness probe that starts checking before a slow-booting application is ready. Use a startup probe for boot time and keep the liveness probe for genuine hangs:
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 10 # allows up to 5 minutes to start
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10
failureThreshold: 3
While a startup probe is running, the liveness probe is disabled — which is exactly the behaviour you want.
Cause 4: Wrong Command or Entrypoint (exit 127 or 126)
If there are no application logs at all, the process may never have run. Check the events rather than the logs:
Warning Failed 10s kubelet Error: failed to create containerd task: failed to create shim task: OCI runtime create failed: exec: "/app/start.sh": stat /app/start.sh: no such file or directory
Confirm what the image expects by inspecting it, rather than guessing:
docker inspect myrepo/api:v1.2.3 --format '{{.Config.Entrypoint}} {{.Config.Cmd}}'
exec format error in place of the above means an architecture mismatch — an amd64 image on arm64 nodes, or the reverse. Build multi-arch images or pin the node architecture.
Cause 5: The Container Exits Successfully (exit 0)
Exit code 0 with restarts climbing means the container did its job and stopped — and the default restartPolicy: Always restarted it anyway. Kubernetes Deployments expect a long-running process.
If the workload is genuinely a one-shot task, it should be a Job, not a Deployment:
apiVersion: batch/v1
kind: Job
metadata:
name: migrate
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: migrate
image: myrepo/api:v1.2.3
command: ["./migrate"]
How to Pause the Crash Loop and Debug
Deleting the pod does not help — the controller recreates it, and you lose the previous logs. Scale down instead:
kubectl scale deployment/api --replicas=0
Then run a debug copy that stays up regardless of whether the app works:
kubectl run debug --rm -it --image=myrepo/api:v1.2.3 --restart=Never \
--command -- sleep 3600
In another terminal, exec in and reproduce the startup by hand:
kubectl exec -it debug -- sh
On clusters that support it, kubectl debug attaches an ephemeral container to the real pod, keeping its actual environment and volumes:
kubectl debug -it api-6b8f7d9c5d-h4trm --image=busybox --target=api
Verify the Fix
kubectl rollout restart deployment/api
kubectl rollout status deployment/api
kubectl get pods -w
A fixed pod reaches Running with 1/1 ready and a restart count that stops climbing. Watch it for a few minutes before declaring victory — the backoff only resets after the container has stayed up for ten minutes, so a pod that survives thirty seconds has not necessarily recovered.
kubectl get pod -l app=api -o wide
Prevention
- Set memory requests and limits deliberately, and make runtimes cgroup-aware (
-XX:MaxRAMPercentagefor the JVM; recent Node versions read the limit automatically). - Use startup probes for slow boots rather than inflating
initialDelaySecondson the liveness probe. - Fail loudly and early on missing configuration. An application that validates its environment at startup and prints exactly what is missing turns a fifteen-minute investigation into a ten-second one.
- Retry external dependencies with backoff instead of exiting on the first connection refusal.
- Alert on restart counts, not just on Ready. A pod that crashes and recovers every few minutes can stay technically available while dropping requests the whole time.