TL;DR
CrashLoopBackOffmeans the container keeps starting and exiting. The status is the kubelet's backoff timer, not the root cause.- Diagnose in this order:
kubectl get pods→kubectl describe pod(Events + Last State exit code) →kubectl logs --previous. - Map the exit code:
1/2app or config error,137often OOMKilled,143often SIGTERM from a probe or graceful kill,127missing binary or bad command. - Do not delete the pod to "fix" it. Fix the crash reason, then verify restarts stop climbing and the pod stays
Running/1/1.
You deploy a new image, kubectl get pods shows CrashLoopBackOff, and the restart count climbs every few minutes. Deleting the pod feels productive, but the replacement comes back with the same status. The dashboard says the rollout failed. It does not say whether the process panicked, the memory limit is too low, or a liveness probe is killing a slow starter.
In this article, you will reproduce a crash loop in a sandbox namespace, read Last State and previous logs, fix the three causes you hit most often (app/config exit, OOMKilled, bad liveness probe), and verify the pod stays healthy.
Note
What you need: kubectl 1.28+, access to a cluster (kind, minikube, EKS, GKE, or AKS), and permission to create namespaces and Deployments in a non-production sandbox. The walkthrough uses namespace demo-app.
Why pods enter CrashLoopBackOff
Kubernetes starts the container. The process exits with a non-zero code (or is killed). The kubelet restarts it according to restartPolicy (usually Always for Deployments). After repeated failures, the kubelet waits longer between attempts. That waiting state is CrashLoopBackOff.
The Kubernetes debugging docs treat this as a symptom. Your job is to find why the last container terminated.
| Status | What it means |
|---|---|
Error / CrashLoopBackOff | Container exited; kubelet is restarting with backoff |
OOMKilled (in Last State) | Container exceeded its memory limit |
CreateContainerConfigError | Pod never started (missing Secret/ConfigMap reference). Not a crash loop |
ImagePullBackOff | Image never arrived. Different failure mode |
Use this exit-code map when you read Last State from kubectl describe pod:
| Exit code | Typical meaning | Where to look next |
|---|---|---|
0 | Clean exit (unexpected for a long-running server) | Command completed; check if the process is a one-shot job in a Deployment |
1 / 2 | Application or config error | kubectl logs --previous |
127 | Command or binary not found | command / args vs image contents |
137 | SIGKILL (often OOMKilled) | Last State reason, memory limits |
143 | SIGTERM (often probe or graceful shutdown) | Events for Liveness probe failed |
Warning
Deleting the pod resets the backoff clock. It does not change the crash reason. Fix the root cause first.
How to diagnose with describe and previous logs
Start with status, then Last State, then previous logs. Guessing from the Deployment YAML alone wastes time.
Create a namespace and a Deployment that exits immediately so you can practice the workflow:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: demo-app
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: busybox:1.36
command: ["sh", "-c", "echo 'FATAL: missing DATABASE_URL' >&2; exit 1"]Apply it and watch the status flip to CrashLoopBackOff:
kubectl create namespace demo-app
kubectl apply -f crash-deployment.yaml
kubectl get pods -n demo-app -wYou should see STATUS move through Error into CrashLoopBackOff while RESTARTS climbs.
Describe the pod and read two sections: Last State (reason + exit code) and Events:
kubectl describe pod -n demo-app -l app=apiExpect output shaped like this:
Last State: Terminated
Reason: Error
Exit Code: 1
Started: Sat, 11 Jul 2026 10:01:02 +0000
Finished: Sat, 11 Jul 2026 10:01:02 +0000
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Created 12s (x3 over 40s) kubelet Created container: api
Normal Started 12s (x3 over 40s) kubelet Started container api
Warning BackOff 5s (x4 over 39s) kubelet Back-off restarting failed container api in pod api-...Pull logs from the container that already died. Current logs are often empty while the new instance is still starting:
kubectl logs -n demo-app -l app=api --previousYou should see:
FATAL: missing DATABASE_URLThat string is the fix target. Everything below maps an exit code or Event pattern to a concrete change.
How to fix application and config crashes
Exit codes 1 and 2 with a clear log line usually mean the process refused to start: missing env, bad config file, failed migration, or an unhandled exception.
Confirm referenced ConfigMaps and Secrets exist in the same namespace, and that key names match what the app reads. A typo in secretKeyRef.key often surfaces as a boot failure in logs, or as CreateContainerConfigError before the container ever starts. For how ConfigMaps and Secrets differ, see Kubernetes ConfigMap vs Secret Explained.
Fix the crash Deployment by exporting the required variable and staying up:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: demo-app
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: busybox:1.36
command:
- sh
- -c
- |
if [ -z "$DATABASE_URL" ]; then
echo "FATAL: missing DATABASE_URL" >&2
exit 1
fi
echo "api listening"
sleep 3600
env:
- name: DATABASE_URL
value: postgres://app:secret@db:5432/appApply and wait for the rollout:
kubectl apply -f fixed-app-deployment.yaml
kubectl rollout status deployment/api -n demo-app
kubectl get pods -n demo-appSTATUS should be Running and RESTARTS should stop increasing.
If logs show a stack trace from your language runtime, reproduce with the same image and env locally (docker run --rm -e ... image:tag) before changing cluster YAML again. Wrong command or args that point at a missing binary usually show exit 127 instead.
How to fix OOMKilled
When Last State shows Reason: OOMKilled or exit code 137, the kubelet (via the runtime) killed the container for exceeding its memory limit. Raising the limit without measuring usage only hides a leak until the next traffic spike.
Reproduce with a tiny limit so you can see the pattern safely:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: demo-app
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: busybox:1.36
command:
- sh
- -c
- |
# Allocate until the limit kills the process
dd if=/dev/zero of=/dev/shm/blob bs=1M count=64
sleep 3600
resources:
requests:
memory: "16Mi"
limits:
memory: "32Mi"After apply, describe the pod. You want Last State: Terminated with Reason: OOMKilled.
Raise the limit to a value the workload can actually use, and keep a request so the scheduler places it correctly:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: demo-app
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
resources:
requests:
memory: "64Mi"
limits:
memory: "256Mi"Apply, then confirm Last State is no longer OOMKilled and the pod stays Running.
In production, pair the limit change with metrics (container_memory_working_set_bytes) so you know whether you fixed sizing or papered over a leak.
How to fix failing liveness probes
A healthy process can still CrashLoop if the liveness probe fails. The kubelet kills the container, Events show Liveness probe failed, and the exit code is often 137 or 143 depending on whether SIGKILL or SIGTERM won.
This Deployment listens too late for an aggressive probe:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: demo-app
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: busybox:1.36
command:
- sh
- -c
- |
sleep 20
nc -lk -p 8080 -e /bin/true
ports:
- containerPort: 8080
livenessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
failureThreshold: 1Describe the pod after it loops. Events should mention Liveness probe failed. Stretching initialDelaySeconds forever is the wrong long-term fix for slow apps.
Add a startupProbe so liveness (and readiness) stay off until startup succeeds, then keep a sane liveness check:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: demo-app
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: nginx:1.25-alpine
ports:
- containerPort: 80
startupProbe:
httpGet:
path: /
port: 80
failureThreshold: 30
periodSeconds: 2
livenessProbe:
httpGet:
path: /
port: 80
periodSeconds: 10
failureThreshold: 3Apply and confirm Events no longer show repeated liveness kills.
Two probe rules prevent most of these incidents:
- Do not probe external dependencies (databases, third-party APIs) in liveness. A brief dependency blip should not restart every replica.
- Use
startupProbefor anything that needs more than a few seconds to boot. That is what the configure liveness, readiness, and startup probes docs recommend.
How to verify the pod is healthy
A real fix shows up in three places:
- Status:
kubectl get pods -n demo-appshowsRunningandREADY1/1. - Restarts: The
RESTARTScolumn stops climbing for several minutes. - Events / logs: No new
BackOff,OOMKilled, orLiveness probe failedlines; current logs show the process staying up.
Useful checks:
kubectl get pods -n demo-app
kubectl describe pod -n demo-app -l app=api | sed -n '/Containers:/,/Conditions:/p'
kubectl logs -n demo-app -l app=api --tail=50
kubectl rollout status deployment/api -n demo-appIf the crash started right after a deploy and you need traffic back while you debug, roll back first, then fix forward:
kubectl rollout undo deployment/api -n demo-app
kubectl rollout status deployment/api -n demo-appWhen these fixes do not apply
CrashLoopBackOff always means the container exited, but not every exit is app code, OOM, or probes.
- Init container failure: The app container never starts. Describe the pod and check init container Last State and logs (
kubectl logs POD -c INIT_NAME). - Missing volume permissions: A non-root container cannot write a mounted path. Fix
securityContext(runAsUser,fsGroup) or volume permissions rather than raising memory. - Dependency not ready at boot: The app exits if the database is down and has no retry. Fix the app startup policy or gate with a check that does not kill the process forever on first failure.
- Wrong image architecture: Pull succeeds, then the container dies with
exec format error. Rebuild for the node architecture. If the image never arrives, you are in ImagePullBackOff, not CrashLoopBackOff. - Node pressure / eviction: The pod may be restarted for reasons outside your container. Check node conditions and cluster autoscaling events, not only container logs.
For authoritative reference, see the Kubernetes docs on debugging pods and GKE CrashLoopBackOff troubleshooting.
Frequently asked questions
Q: What does CrashLoopBackOff mean?
It means the container keeps crashing and the kubelet is delaying restarts. Find the cause in Last State, Events, and kubectl logs --previous.
Q: Should I delete the pod?
No. Deletion only resets backoff. Fix the crash reason so the next start succeeds.
Q: Why are current logs empty?
The live container may have just started. Read --previous logs from the instance that exited.
Q: CrashLoopBackOff vs ImagePullBackOff?
ImagePullBackOff means the image never pulled. CrashLoopBackOff means the image ran and the process exited. Diagnose pull failures with Events; diagnose crash loops with Last State and previous logs.
Q: Exit 137 always means OOM?
Usually it is SIGKILL, and OOMKilled is the common Kubernetes case. Confirm with Reason: OOMKilled in describe. Probe failures and stubborn processes can also end in 137 after SIGTERM times out.

