All articles
Tutorials

How to Fix the "CrashLoopBackOff" Error in Kubernetes

Learn how to diagnose CrashLoopBackOff with kubectl describe and logs --previous, then fix app crashes, OOMKills, and failing liveness probes.

How to Fix the "CrashLoopBackOff" Error in Kubernetes cover
10 min read

TL;DR

  • CrashLoopBackOff means 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/2 app or config error, 137 often OOMKilled, 143 often SIGTERM from a probe or graceful kill, 127 missing 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.

StatusWhat it means
Error / CrashLoopBackOffContainer exited; kubelet is restarting with backoff
OOMKilled (in Last State)Container exceeded its memory limit
CreateContainerConfigErrorPod never started (missing Secret/ConfigMap reference). Not a crash loop
ImagePullBackOffImage never arrived. Different failure mode

Use this exit-code map when you read Last State from kubectl describe pod:

Exit codeTypical meaningWhere to look next
0Clean exit (unexpected for a long-running server)Command completed; check if the process is a one-shot job in a Deployment
1 / 2Application or config errorkubectl logs --previous
127Command or binary not foundcommand / args vs image contents
137SIGKILL (often OOMKilled)Last State reason, memory limits
143SIGTERM (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:

crash-deployment.yaml
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 -w

You 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=api

Expect 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 --previous

You should see:

FATAL: missing DATABASE_URL

That 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:

fixed-app-deployment.yaml
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/app

Apply 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-app

STATUS 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:

oom-deployment.yaml
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:

oom-fixed-deployment.yaml
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:

probe-crash-deployment.yaml
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: 1

Describe 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:

probe-fixed-deployment.yaml
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: 3

Apply and confirm Events no longer show repeated liveness kills.

Two probe rules prevent most of these incidents:

  1. Do not probe external dependencies (databases, third-party APIs) in liveness. A brief dependency blip should not restart every replica.
  2. Use startupProbe for 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:

  1. Status: kubectl get pods -n demo-app shows Running and READY 1/1.
  2. Restarts: The RESTARTS column stops climbing for several minutes.
  3. Events / logs: No new BackOff, OOMKilled, or Liveness probe failed lines; 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-app

If 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-app

When 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.

Frequently asked questions

Share𝕏

Writer

  • Ilyas Rufai

    Technical content writer and DevSecOps specialist focused on cloud-native security and developer experience

Need help with your technical content?

We help B2B SaaS teams turn complex products into clear documentation and content that developers actually use.

Book a call
How to Fix CrashLoopBackOff in Kubernetes | Reclear