Home › DevOps › CrashLoopBackOff: Fix a K8s Pod Stuck Restarting
Intermediate 7 min · September 23, 2026

CrashLoopBackOff: Fix a K8s Pod Stuck Restarting

Run kubectl logs --previous to find crash cause, then fix the command, config, or probe.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.

Follow
✓ Production
production tested
September 23, 2026
last updated
1,942
articles · all by Naren
Before you start⏱ 14 min
  • ✓A running Kubernetes cluster you can inspect (minikube, kind, or a dev namespace)
  • ✓kubectl installed and pointing at that cluster
  • ✓Basic comfort reading a Deployment manifest and pod events
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • Fix it with kubectl logs --previous and kubectl describe pod, then correct the crash cause — usually a bad command, missing env var, or failing probe — and reapply the Deployment.
  • CrashLoopBackOff isn't an error, it's the kubelet spacing restarts further apart (10s, 20s, 40s, up to 5 min) while your container keeps dying.
  • Read the exit code: 137 means OOMKilled (raise memory limits), 1 means your app threw an error (read the logs), 0 means it exited cleanly (it belongs in a Job).
  • Don't confuse it with Error (fresh crashes, no backoff yet), OOMKilled (memory kill), or CreateContainerConfigError (never started — bad mount or reference).
✦ Definition~90s read
What is Kubernetes CrashLoopBackOff Fix?

CrashLoopBackOff is a pod state set by the kubelet — the node agent responsible for keeping your containers running. When a container terminates, the kubelet restarts it per the pod's restartPolicy (Always, the Deployment default). If it keeps dying, the kubelet inserts a growing delay before each retry: 10 seconds, then 20, 40, 80, 160, and finally a 300-second cap.

★
Think of a toaster with a safety timer.

During that waiting period, kubectl reports the container state as CrashLoopBackOff. The mechanism is pure pacing — it protects the node and the API server from a container churning through restarts ten times a second, and it resets to 10 seconds the moment a container stays alive.

What it is NOT matters more than the definition. It is not an error message — there's no CrashLoopBackOff exception in your app, and nothing in your logs will ever say those words. It is not a diagnosis: it tells you the container dies repeatedly but never tells you why, which is why two engineers can stare at the same pod and propose opposite fixes.

It is not OOMKilled (that's a specific death cause, exit 137, from breaching memory limits), not Error (the pre-backoff state of fresh crashes), and not CreateContainerConfigError (where the container never started due to a bad mount or missing object). Most dangerously, it is not a cluster problem — new engineers often suspect the node or the scheduler, but the kubelet is working exactly as designed.

The bug is always in your image, your manifest, or your probes. Read the state as 'your container keeps dying, here's the waiting room' and go find the death cause with kubectl logs --previous.

Plain-English First

Think of a toaster with a safety timer. You push the lever, the toast burns because the dial is wrong, and the lever pops. Push it again — same burn. A smart toaster would make you wait longer between tries so you check the dial instead of burning toast all morning. That's CrashLoopBackOff: your app keeps failing at startup, and Kubernetes waits 10 seconds, then 20, then 40 between retries while you find the real problem — a wrong command, a missing setting, or a health check that's too strict.

Nothing ruins your morning like a deploy that looked green in CI and then parks every new pod in CrashLoopBackOff. The Service has no healthy endpoints, the rollout is stuck, and kubectl get pods shows a restart count climbing while the retry delay stretches further apart. Your instinct says the cluster is broken. It isn't. Somewhere in your manifest or startup path there's a small mistake, and the kubelet is patiently retrying it forever.

CrashLoopBackOff is Kubernetes' most-seen and most-misread pod state. Engineers treat it as an error and start changing things at random: bump memory, delete the pod, relax the probe. Sometimes one of those sticks, but nobody learns why, so the same typo kills the next deploy. The growing delay — 10 seconds, then 20, then 40, up to 5 minutes — feels like the cluster giving up. It's protecting the node while it waits for your fix.

This guide gives you a repeatable way out. You'll learn what the backoff is doing, the five causes behind nearly every loop (bad command, missing config, killer probes, OOMKilled, instant exit 0), and how to tell CrashLoopBackOff from Error, OOMKilled, and CreateContainerConfigError. You'll get a crashing-versus-fixed Deployment pair, a debug sequence built on kubectl logs --previous and describe pod, and the habits — startup probes, data-sized resources, Jobs for finite work — that stop the loop coming back.

CrashLoopBackOff Is a Waiting State, Not an Error

When a container dies, the kubelet doesn't give up — it restarts it. Die again, and it waits 10 seconds. Then 20, 40, 80, 160, capping at 300 seconds (5 minutes). That growing pause is the 'BackOff' in CrashLoopBackOff, and it exists to protect the node: without it, a pod crash-looping ten times a second would spam the API server and churn CPU forever. You'll see the state in kubectl get pods while kubectl describe pod shows reason: CrashLoopBackOff, a climbing restartCount, and a lastState.terminated block with the exit code that tells you how the container actually died.

Here's the part most engineers miss: the backoff resets the moment a container stays alive. There's no stuck timer to clear and no node state to flush. Fix the crash cause, and the replacement pod goes Ready on its first try — the hour of accumulated delay doesn't carry over. That's why deleting the pod 'to clear the backoff' never helps: the new pod runs the same broken image and config, crashes the same way, and climbs the same 10s-to-5min ladder. The delay is pure pacing, and pacing is never the problem.

So read the state as a message, not a malfunction. CrashLoopBackOff says: this container starts (or tries to) and dies, over and over, and I'm spacing out retries while you investigate. Your job isn't to fight the scheduler — it's to find the death cause in the previous container's logs and the exit code, fix it in the manifest or image, and roll forward. Everything else in this guide is about doing exactly that as fast as possible.

⚠ The Delay Is a Symptom, Not the Disease
Don't chase the delay — no kubectl command clears backoff faster than fixing the crash. Deleting the pod just starts the same 10s, 20s, 40s climb over again on its replacement.
📊 Production Insight
During a lunch-rush incident, an engineer deleted crash-looping pods three times hoping to 'reset the backoff'. Each replacement climbed the same delay ladder because the bad CLI flag was still in the manifest. The loop ended 20 minutes later when someone read the --previous logs instead. Rule: if you haven't changed the manifest or image, don't bother deleting the pod.
🎯 Key Takeaway
CrashLoopBackOff is pacing, not breakage: restarts spaced 10s to 5min while you fix the death cause. It resets on success, so fix the crash — never chase the delay.

Bad Command or Args: the One-Line Killer

The single most common crash-loop cause is also the most embarrassing: the container's command doesn't work. Maybe the manifest overrides command with a binary that isn't in the image (python when only python3 exists), passes a flag the app renamed last sprint, or points at a file that moved (python app.py when the code now lives at src/main.py). The container starts, the entrypoint fails instantly with 'executable not found' or a usage error, and you get exit code 1 or 2 within a second. Three of those in a row and you're in backoff.

Compare the pair below. The broken Deployment passes --port=8080, which argparse rejects because the app now expects --listen-port — the container exits code 2 two seconds after start, every time. The fixed Deployment passes the flag the image actually accepts. Nothing else changed: same image, same resources, same probes. That's typical — crash loops from bad commands are one-line fixes once you see the usage error in kubectl logs <pod> --previous.

Build the habit that prevents this class entirely: run the exact command locally with docker run before it ever reaches the cluster, and add a CI smoke step that boots the image with --help or a dry-run flag. If the entrypoint is wrong, CI should go red — not your checkout Service at noon. And when you inherit someone else's image, inspect its real entrypoint (docker inspect or the Dockerfile CMD) instead of guessing what the binary is called.

checkout-deployment-fixed.yamlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# BROKEN: passes a flag the image no longer accepts -> exit code 2 in ~2s
apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout
  labels:
    app: checkout
spec:
  replicas: 3
  selector:
    matchLabels:
      app: checkout
  template:
    metadata:
      labels:
        app: checkout
    spec:
      containers:
        - name: checkout
          image: registry.example.com/shop/checkout:2.4.1
          args: ["--serve", "--port=8080"]   # stale flag: app wants --listen-port
          ports:
            - containerPort: 8080
---
# FIXED: identical except the flag matches the image's real interface
apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout
  labels:
    app: checkout
spec:
  replicas: 3
  selector:
    matchLabels:
      app: checkout
  template:
    metadata:
      labels:
        app: checkout
    spec:
      containers:
        - name: checkout
          image: registry.example.com/shop/checkout:2.4.1
          args: ["--serve", "--listen-port=8080"]  # matches argparse interface
          ports:
            - containerPort: 8080
📊 Production Insight
A team renamed --port to --listen-port during an argparse migration and updated the docs but not deploy/checkout.yaml. All 6 pods crash-looped with exit code 2 for 38 minutes because nobody ran the new image with --help in CI. Rule: any PR that changes CLI flags must update the manifest in the same PR, enforced by a pipeline check.
🎯 Key Takeaway
If the container dies within seconds with a usage or not-found error, the command is wrong. Test the exact command with docker run locally and smoke-test it in CI.

Missing Env Vars and Config That Kill Startup

The second classic is the app that boots, looks for a setting, doesn't find it, and quits. A Python service throws KeyError: 'STRIPE_KEY', a Go binary logs 'required env DATABASE_URL not set' and exits 1, a Java app can't reach postgres because DB_HOST came from a ConfigMap in the wrong namespace. The container technically runs — it just commits suicide within seconds because its world is incomplete. kubectl logs --previous shows the complaint plainly, usually in the last five lines before the traceback.

Note the sharp edge here: a manifest that references a ConfigMap or Secret that doesn't exist at all fails differently — Kubernetes won't even start the container and you'll see CreateContainerConfigError instead. CrashLoopBackOff from config means the reference resolved but the content disappointed: a typo'd key (datebase_url), a value the app can't parse, or a Secret that exists but holds last quarter's rotated password. The container started, read its config, hated it, and left.

The fixed manifest below wires config explicitly: required values come from a Secret and a ConfigMap that live in the same namespace, with names spelled exactly as created. Pair that with fail-fast startup code — validate every required variable on boot and log its absence by name — and the next missing setting announces itself in one log line instead of a bare traceback. Validate references before applying: a quick kubectl get configmap,secret plus a dry-run render catches the typo'd key while it's still cheap.

payments-deployment-env-fixed.yamlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# FIXED: explicit env wiring — Secret + ConfigMap in the SAME namespace as the pod
apiVersion: apps/v1
kind: Deployment
metadata:
  name: payments
  namespace: production
  labels:
    app: payments
spec:
  replicas: 2
  selector:
    matchLabels:
      app: payments
  template:
    metadata:
      labels:
        app: payments
    spec:
      containers:
        - name: payments
          image: registry.example.com/shop/payments:1.9.0
          ports:
            - containerPort: 8080
          env:
            - name: STRIPE_KEY               # required: app fails fast naming it if absent
              valueFrom:
                secretKeyRef:
                  name: payments-secrets     # must exist in namespace: production
                  key: stripe-key
            - name: DB_HOST
              valueFrom:
                configMapKeyRef:
                  name: payments-config      # must exist in namespace: production
                  key: db-host
# Pre-apply check (same namespace!):
# kubectl get secret payments-secrets -n production
# kubectl get configmap payments-config -n production
📊 Production Insight
An on-call engineer spent an hour bumping memory on a pod whose --previous log said KeyError: 'REDIS_URL' on line one. The Secret existed — in staging, not production. Rule: when the log names a variable, check the object in the pod's own namespace first; cross-namespace references don't exist for env and volumes.
🎯 Key Takeaway
Crash loops from config mean the container started but hated its settings. Name every required variable at startup and validate ConfigMap/Secret references before applying.

Liveness Probes That Kill Healthy Apps

Liveness probes restart containers — that's their entire job. So when a liveness probe is misconfigured, it manufactures a crash loop out of a perfectly healthy app. The usual story: initialDelaySeconds is 5 but the app needs 45 seconds to warm its cache (longer under CPU-throttled morning load), so the kubelet starts probing a half-booted process, gets connection refused three times, and SIGTERMs a container that was 10 seconds from Ready. The app log shows a normal startup chopped off mid-line. Describe shows 'Liveness probe failed' events marching in step with the restarts.

Keep the two probe types straight and this class disappears. Readiness controls traffic: fail it and the pod leaves the Service endpoints, no restart. Liveness controls life: fail it past failureThreshold and the kubelet kills the container, which under restartPolicy: Always means a restart — and repeated restarts mean backoff. Never gate boot-time on liveness. The manifest below does it right: a startupProbe owns the boot window (up to 60 checks × 5s = 5 minutes of grace), liveness only starts after startup succeeds, and readiness stays independent so a slow dependency removes the pod from traffic instead of killing it.

Size every number from measurement, not hope. Time your worst-case cold start under load (p99, not the laptop average), then set startup failureThreshold above it. A probe tuned on a developer's laptop with a warm image cache will murder the same app on a cold node at 9 AM — and you'll spend the morning debugging an app that was never broken.

catalog-deployment-probes-fixed.yamlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# FIXED: startup probe owns the boot window; liveness only judges steady state
apiVersion: apps/v1
kind: Deployment
metadata:
  name: catalog
  labels:
    app: catalog
spec:
  replicas: 3
  selector:
    matchLabels:
      app: catalog
  template:
    metadata:
      labels:
        app: catalog
    spec:
      containers:
        - name: catalog
          image: registry.example.com/shop/catalog:3.2.0
          ports:
            - containerPort: 8080
          startupProbe:                        # owns boot: 60 x 5s = 5 min grace
            httpGet:
              path: /healthz
              port: 8080
            periodSeconds: 5
            failureThreshold: 60
          livenessProbe:                       # starts only after startup succeeds
            httpGet:
              path: /healthz
              port: 8080
            periodSeconds: 15
            failureThreshold: 3
          readinessProbe:                      # traffic only, never restarts
            httpGet:
              path: /ready
              port: 8080
            periodSeconds: 10
            failureThreshold: 2
📊 Production Insight
A Java service with a 90-second cold start ran liveness with initialDelaySeconds: 10. Every morning deploy killed each pod 3 times before one got lucky on a warm node. Adding a startupProbe with failureThreshold: 30 ended the loop overnight. Rule: if restarts cluster around deploys and cold nodes, the probe is the killer — measure p99 boot before tuning.
🎯 Key Takeaway
Liveness kills, readiness only detaches. Give boot time to a startup probe sized from p99 cold starts, and never let liveness judge a half-booted app.

Exit Codes and Lookalikes: 137, Error, OOMKilled, Config Errors

Exit codes are the fastest triage tool you have, so learn the four you'll actually meet. Exit 1 (or 2) is your app raising its hand: an unhandled exception, a bad flag, a failed assertion — the answer is in the --previous logs. Exit 137 (128 + 9, SIGKILL) is the kernel OOM-killing your container for breaching limits.memory: the process vanishes mid-request with no error because it never got to log one. Exit 0 is success — which under restartPolicy: Always still triggers a restart, looping forever. And 'no exit code at all' plus CreateContainerConfigError means the container never started: bad volume mount, missing ConfigMap, or an env reference to an object that doesn't exist.

The lookalikes matter because each sends you to a different layer. Error (without backoff) is just the opening act — the first crashes before the delay ladder starts; debug it identically. OOMKilled is a resource verdict, not an app verdict: confirm with the reason field and memory metrics, then raise limits.memory above peak plus headroom or fix the leak. CreateContainerConfigError is a manifest verdict: kubectl describe pod names the missing object ('configmap "x" not found'), and the fix is creating it or correcting the reference — no amount of log-reading helps because there's no container to have logged.

The command block below pulls all of this in three queries: exit codes and reasons straight from the pod status, the events that separate probe kills from instant crashes, and live memory against the limit. Run them in order and you'll know which of the four you're holding within a minute — then jump to that section's fix instead of guessing.

crashloop-triage.shBASH
1
2
3
4
5
6
7
8
9
# 1) Exit codes + reasons: the 10-second triage (137 = OOM, 1/2 = app, 0 = finished)
kubectl get pod checkout-7d9c8f6b4c-x2v4p -o jsonpath='{range .status.containerStatuses[*]}{.name} restarts={.restartCount} lastExit={.lastState.terminated.exitCode} reason={.lastState.terminated.reason}{"\n"}{end}'

# 2) Events: probe kills vs instant crashes vs missing objects
kubectl describe pod checkout-7d9c8f6b4c-x2v4p | grep -E 'Reason|Message|probe|Back-off|OOM|not found' | tail -20

# 3) Memory vs limit: confirm the OOM story before raising anything
kubectl top pod checkout-7d9c8f6b4c-x2v4p --containers
kubectl get pod checkout-7d9c8f6b4c-x2v4p -o jsonpath='{range .spec.containers[*]}{.name} limits={.resources.limits.memory}{"\n"}{end}'
💡137 Means the Kernel Did It
Exit 137 with no app error? Stop reading application logs — they can't show you a death the kernel dealt from outside. Check memory usage against limits.memory first, every time.
📊 Production Insight
Two engineers once debugged the same pod for an hour — one adding try/except blocks for an app error, the other raising memory. The exit code was 137 the whole time, visible in one jsonpath query. Rule: quote the exit code out loud before anyone proposes a fix; it ends framework debates instantly.
🎯 Key Takeaway
Read the exit code first: 1 means app error, 137 means OOM kill, 0 means it wants a Job, no code means it never started. Each code picks your next command.

The Debug Sequence: From Symptom to Fixed Deploy

When the pager goes off, run this sequence exactly — it resolves nearly every crash loop in under ten minutes. First, kubectl logs <pod> --previous: the dead container's last words, where usage errors and tracebacks live. Second, kubectl describe pod: exit codes, restart count, and the event stream that separates probe kills ('Liveness probe failed') from instant deaths from missing objects. Third, the exit-code query from the previous section to name your category: 1/2, 137, 0, or never-started. By step three you know which fix to apply, and you've spent maybe three minutes.

Fourth, fix forward in the manifest — never hand-edit the crashing pod, which a controller will replace or which teaches you nothing reproducible. Bad flag? Correct args and apply. Missing config? Create the object in the right namespace and roll out. Probe killing boot? Add the startup probe. OOM? Raise limits.memory with headroom. Fifth, verify with kubectl rollout status plus kubectl get events scoped to the new pods, and confirm restart counts stay at zero for two full minutes. A pod that survives one backoff window (5 minutes worst case) is fixed; one that dies at 90 seconds will fool a 60-second glance.

Make the loop structurally unlikely to return. Gate rollouts with readiness probes and maxUnavailable: 1 so a bad revision can't empty the Service. Run finite work in Jobs, not Deployments. Smoke-test the image's real command in CI. These five habits don't just fix today's loop — they delete the whole category of 2 AM pages where the cluster retries your typo forever while you sleep.

crashloop-debug-sequence.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# The 5-step crash-loop sequence — run in order, under 10 minutes
# 1) Why did it die? (dead container's last words)
kubectl logs checkout-7d9c8f6b4c-x2v4p --previous --tail=50

# 2) Context: exit codes, restart count, event stream
kubectl describe pod checkout-7d9c8f6b4c-x2v4p | tail -40

# 3) Name the category, then fix FORWARD in the manifest (never hand-edit pods)
#    exit 1/2 -> fix command/args or app bug
#    exit 137 -> raise limits.memory
#    exit 0   -> move workload to a Job
kubectl apply -f checkout-deployment-fixed.yaml
kubectl rollout status deploy/checkout --timeout=180s

# 4) Verify: new pods stay at zero restarts for 2+ minutes
kubectl get pods -l app=checkout -w
kubectl get events --field-selector involvedObject.kind=Pod -w | grep -iE 'fail|kill|oom|back-off'
📊 Production Insight
The fastest incident response on record for this team was 6 minutes: logs showed the bad flag at 0:31, describe confirmed exit 2 at 1:05, manifest fix applied at 3:40, rollout green at 5:55. Rule: the sequence works because each step eliminates a whole category — skipping steps is what turns 6 minutes into 38.
🎯 Key Takeaway
Logs --previous, describe, exit code, fix forward in the manifest, verify for two minutes. Then add rollout gates and CI smoke tests so the category can't recur.
● Production incidentPOST-MORTEMseverity: high

A Renamed CLI Flag Restarted Checkout Pods 214 Times at Lunch

Symptom
At 12:04 PM, right after a routine deploy, checkout success rate fell to zero. kubectl get pods showed all 6 checkout pods in CrashLoopBackOff with restart counts climbing past 50 within minutes. No CPU or memory alerts fired — usage was flat because the containers died 2 seconds after starting. The only signal was the pod state and a flood of 'Back-off restarting failed container' events.
Assumption
The on-call engineer assumed the container registry was having an outage. The deploy had changed nothing in the app code — just a Dockerfile cleanup — so a bad image pull felt likely. They spent 20 minutes checking registry status pages and re-pulling the previous tag, which worked fine locally and deepened the confusion.
Root cause
The Dockerfile cleanup had switched the app from sys.argv parsing to argparse and renamed --port to --listen-port, but deploy/checkout.yaml still passed args: ["--serve", "--port=8080"]. argparse exited code 2 with 'unrecognized arguments: --port=8080' about 2 seconds after start. The kubelet restarted the container, the same bad flag killed it again, and after the third rapid crash the pod entered CrashLoopBackOff with delays doubling toward the 5-minute cap. All 6 replicas ran the identical bad args, so the Service lost every endpoint at once.
Fix
The fix was a one-line manifest change plus a controlled rollout. They corrected args to ["--serve", "--port=8080"] to match the new argparse interface, ran kubectl apply -f deploy/checkout.yaml, then kubectl rollout status deploy/checkout --timeout=180s. All 6 pods went Ready within 90 seconds, restart counts stopped at 214 on the old ReplicaSet, and error rate dropped from 100 percent to baseline in under 3 minutes. They also pinned the deploy pipeline to run the container with --help as a smoke test so a renamed flag fails CI instead of production.
Key lesson
  • Always smoke-test the exact container command in CI — running the image with --help or a dry-run flag would have caught the renamed argument before it reached the cluster.
  • Read kubectl logs --previous before theorizing: the 'unrecognized arguments' error was in the first three log lines, and 20 minutes of registry investigation never looked at them.
  • Gate rollouts with readiness probes and maxUnavailable: 1 so a crashing revision can't take every replica at once — here all 6 pods restarted simultaneously and emptied the Service.
Production debug guideFive symptoms, five exact command sequences — run them in order and you'll land on the cause.5 entries
Symptom · 01
Pod shows CrashLoopBackOff with a climbing restart count and you don't know why it dies
→
Fix
Pull the last dead container's output — that's where the real error lives. Then check the exit code: 1 means your app threw (fix code or config), 137 means the kernel OOM-killed it (jump to the OOM item), 0 means it finished successfully (it wants a Job, not a Deployment). If --previous is empty, the container died before logging — check describe output next.
Symptom · 02
kubectl logs is empty and you can't tell if the container ever started
→
Fix
The events tell you whether the container ever started. 'Failed to start container' or 'Error: executable not found' means your command or args don't match the image — compare against the Dockerfile CMD and fix the manifest. If instead you see 'Started container' followed by 'Back-off restarting', the container runs then dies: go back to the logs and exit code, the problem is inside the app or its config.
Symptom · 03
Exit code 137 — the container vanishes mid-request with no error in the logs
→
Fix
Confirm the kill with the exit-code query above (reason OOMKilled, exit 137), then check live usage against the limit. Fix: raise limits.memory above the observed peak plus at least 25 percent headroom, fix the leak if usage grows without bound, and set requests.memory to the steady-state value. Reapply and watch the restart count stop climbing.
Symptom · 04
App logs look healthy but the container gets SIGTERM'd halfway through startup
→
Fix
Describe is the differentiator: repeated 'Liveness probe failed' events with a container that logs a normal-but-interrupted startup means the probe is the killer, not the app. Fix: add a startupProbe (or raise initialDelaySeconds past your measured worst-case boot time under load) and keep readiness separate. Reapply, then verify with kubectl get events --field-selector involvedObject.name=<pod> that probe failures stop.
Symptom · 05
You're not sure if it's CrashLoopBackOff, Error, OOMKilled, or CreateContainerConfigError
→
Fix
Read the pod's reason field: 'CrashLoopBackOff' means it started and died repeatedly (app or config problem — use the items above). 'Error' means early crashes before backoff kicked in (same debugging, just fresher). 'CreateContainerConfigError' means it never started — a missing ConfigMap/Secret, bad volume mount, or invalid env reference (fix the manifest reference). 'OOMKilled' with 137 means memory (raise limits). Match the reason first, then debug that layer.
CrashLoopBackOff Causes — Confirm, Fix, Prevent
Root CauseHow to ConfirmFixPrevention
Bad container command or argskubectl logs <pod> --previous shows 'executable not found', 'no such file', or a usage error in the first linesCorrect command and args in the Deployment to match the image's real entrypoint, then reapplyTest the exact command with docker run locally; lint manifests in CI
Missing env var, ConfigMap, or SecretLogs show KeyError, 'env var required', or 'connection refused' to a host that should come from configCreate or correct the ConfigMap/Secret, fix the key names and namespace, then roll outFail fast with a clear startup error; validate required config in CI before deploy
Failing liveness probekubectl describe pod shows 'Liveness probe failed' events and the app log shows startup cut off by SIGTERMAdd a startup probe or raise initialDelaySeconds past measured worst-case startup timeLoad-test startup time and set probe delays from data, not guesses
OOMKilled (exit code 137)kubectl get pod -o jsonpath shows reason OOMKilled with exit 137; metrics show memory climbing to the limitRaise limits.memory above peak plus headroom and fix leaks; consider requests equal to limitsSet memory limits from load-test peaks and alert at 80 percent of limit
App exits 0 immediatelyLogs show normal completion and kubectl describe pod shows exit code 0 with a climbing restart countMove the workload to a Job with restartPolicy OnFailure instead of a DeploymentChoose the controller by workload shape: Jobs finish, Deployments serve
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
checkout-deployment-fixed.yamlapiVersion: apps/v1Bad Command or Args
payments-deployment-env-fixed.yamlapiVersion: apps/v1Missing Env Vars and Config That Kill Startup
catalog-deployment-probes-fixed.yamlapiVersion: apps/v1Liveness Probes That Kill Healthy Apps
crashloop-triage.shkubectl get pod checkout-7d9c8f6b4c-x2v4p -o jsonpath='{range .status.containerS...Exit Codes and Lookalikes
crashloop-debug-sequence.shkubectl logs checkout-7d9c8f6b4c-x2v4p --previous --tail=50The Debug Sequence

Key takeaways

1
CrashLoopBackOff is a kubelet restart delay, not an error
the crash cause is what you fix.
2
Always start with kubectl logs <pod> --previous, then kubectl describe pod for exit codes and events.
3
Exit 137 means OOMKilled (raise memory), exit 1 means app error (read logs), exit 0 means it belongs in a Job.
4
Liveness probes restart containers; readiness probes only control traffic
a bad liveness probe loops healthy apps.
5
CreateContainerConfigError means the container never started; CrashLoopBackOff means it started and died repeatedly.
6
Prevent loops with startup probes, memory limits sized from load tests, and Jobs for work that finishes.

Common mistakes to avoid

6 patterns
×

Editing probes and resource limits before reading the previous container's logs

Symptom
You bump memory limits and relax the liveness probe, but the pod keeps crashing. Hours later someone runs kubectl logs --previous and finds a typo in the container command that was visible in the first 30 seconds.
Fix
Run kubectl logs <pod> --previous first. If the log names a missing file or a bad flag, fix the command or args in the Deployment, then kubectl rollout restart deploy/<name>. Don't touch probes or limits until you've read the previous container's log.
×

Overriding command and args without checking the image's real entrypoint

Symptom
Pod crashes instantly with 'executable file not found' or 'no such file'. The image worked for someone else because they never overrode the entrypoint — your command replaced a working default with a binary that doesn't exist in the image.
Fix
Check the image's expected entrypoint with docker inspect or the Dockerfile CMD, then set command and args explicitly in the manifest. Test the exact command locally with docker run before pushing the Deployment.
×

Assuming ConfigMaps and Secrets are mounted when the app starts

Symptom
App throws KeyError or 'required environment variable not set' and exits code 1. The ConfigMap exists but is in the wrong namespace, has a typo'd key, or the Deployment references a name that was never applied.
Fix
Use envFrom with configMapRef plus explicit required vars, and add a startup check in your app that fails fast with a clear message when a required variable is absent. Validate with kubectl exec env or a dry-run render before applying.
×

Setting a liveness probe with no startup grace on a slow-starting app

Symptom
The app logs show a normal startup that gets SIGTERM'd halfway through, every time. Liveness failures climb in lockstep with restarts. The app was never broken — the kubelet killed it before it finished booting.
Fix
Give the app a startup probe or a generous liveness initialDelaySeconds (measure real startup under load first), and keep readiness separate so slow starts only remove the pod from the Service instead of killing it.
×

Treating exit code 137 as an application bug instead of OOMKilled

Symptom
Logs show nothing — the process just vanishes mid-request. Engineers add retry logic and error handling for an error the app never saw, because the kernel killed the container when it breached limits.memory.
Fix
Run kubectl top pods and check containerStatuses for reason OOMKilled and exit 137. Raise limits.memory above the observed peak plus headroom, fix leaks, or set requests equal to limits for critical workloads. Never treat 137 as an app bug first.
×

Running a completed (exit 0) container under restartPolicy Always

Symptom
The container does its job, exits 0, and Kubernetes restarts it forever. Logs look clean, the exit code is success, yet the pod sits in CrashLoopBackOff. The workload wanted a Job, not a Deployment.
Fix
Give one-off tasks restartPolicy: Never or OnFailure in a Job, and reserve restartPolicy: Always plus Deployments for long-running services. If the container is supposed to finish, a Deployment is the wrong controller.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does CrashLoopBackOff actually mean? Is it an error?
Q02SENIOR
Walk me through your exact debug sequence for a pod stuck in CrashLoopBa...
Q03SENIOR
How do you distinguish CrashLoopBackOff from Error, OOMKilled, and Creat...
Q04SENIOR
How can a perfectly healthy app end up in CrashLoopBackOff because of pr...
Q05SENIOR
Your checkout Deployment is crash-looping during peak traffic. How do yo...
Q01 of 05JUNIOR

What does CrashLoopBackOff actually mean? Is it an error?

ANSWER
It's not an error at all — it's a state. The kubelet restarts a repeatedly crashing container with an exponential delay (10s, 20s, 40s, up to 5 minutes) to avoid hammering the node and API server. The real problem is whatever is killing the container; the backoff just paces the retries. Debug with kubectl logs --previous and kubectl describe pod.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Does the backoff delay keep growing forever once a pod is crash-looping?
02
Should I just delete the pod to clear CrashLoopBackOff?
03
Why does kubectl logs show nothing but --previous shows the error?
04
Can a container that exits cleanly with code 0 still go into CrashLoopBackOff?
05
What's the difference between OOMKilled and CrashLoopBackOff?
06
Do readiness probes cause CrashLoopBackOff too?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.

Follow
✓ Verified
production tested
September 23, 2026
last updated
1,942
articles · all by Naren
🔥

That's Kubernetes. Mark it forged?

7 min read · try the examples if you haven't

←
Previous
HTTP 502 Bad Gateway Fix
13 / 15 · Kubernetes
Next
Kubernetes ImagePullBackOff Fix
→