AKS CrashLoopBackOff Explained and Fixed

CrashLoopBackOff is one of the most common states you will see when deploying workloads to Azure Kubernetes Service. It means a container is failing repeatedly on startup. The root cause can be anything from a missing environment variable to an application bug to an out-of-memory kill. This guide provides a systematic diagnostic process using kubectl and AKS-specific tools to identify and fix the underlying cause.

Step 1: Identify the crashing pods

# List all pods in a namespace and show their status
kubectl get pods -n myapp

# Example output showing CrashLoopBackOff
# NAME                          READY   STATUS             RESTARTS   AGE
# api-deployment-7d4b9c-xkpq2   0/1     CrashLoopBackOff   5          8m
# api-deployment-7d4b9c-jlmn3   1/1     Running            0          8m

# Get a broader view across all namespaces
kubectl get pods --all-namespaces --field-selector=status.phase!=Running

# Check restart counts over time
kubectl get pods -n myapp -o wide --show-labels

Note the RESTARTS count. A pod that has restarted 5+ times has been crashing repeatedly. Also note the AGE — a pod that has been crashing for a long time has likely been seen by the team already, while a fresh crash may be related to a recent deployment.

Step 2: Read the previous crash logs

Because the container crashes and is restarted, the logs from the crash are in the previous container instance, not the current one. Use —previous to read them.

# Read logs from the previous (crashed) container instance
kubectl logs -n myapp api-deployment-7d4b9c-xkpq2 --previous

# If there are multiple containers in the pod, specify the container name
kubectl logs -n myapp api-deployment-7d4b9c-xkpq2 --previous -c api-container

# Follow current logs (useful if the container is briefly running before crashing)
kubectl logs -n myapp api-deployment-7d4b9c-xkpq2 -f

# Get the last 100 lines of logs
kubectl logs -n myapp api-deployment-7d4b9c-xkpq2 --previous --tail=100

Common things to look for in crash logs: an unhandled exception or panic, a “port already in use” message, a “cannot connect to database” error, a “required environment variable not set” error, or simply an application printing its help text and exiting (which indicates the wrong command or arguments).

Step 3: Examine the pod description

kubectl describe pod provides the exit code, exit reason, resource usage, and the pod events — all of which are critical for diagnosis.

kubectl describe pod -n myapp api-deployment-7d4b9c-xkpq2

Key sections to read in the output:

Containers:
  api-container:
    ...
    State:          Waiting
      Reason:       CrashLoopBackOff
    Last State:     Terminated
      Reason:       Error          # <-- or OOMKilled, or Completed
      Exit Code:    1              # <-- non-zero = error; 137 = OOMKilled; 1 = app error
      Started:      Thu, 19 Mar 2026 10:14:00 +0000
      Finished:     Thu, 19 Mar 2026 10:14:02 +0000  # <-- 2s runtime = likely startup error

Events:
  Type     Reason     Age                From               Message
  ----     ------     ----               ----               -------
  Warning  BackOff    2m (x8 over 8m)   kubelet            Back-off restarting failed container
  Warning  Failed     8m                kubelet            Error: failed to create containerd task: ...

Exit code 137 means OOMKilled (the process received SIGKILL from the kernel due to memory limit exceeded). Exit code 1 or any non-zero code other than 137 means the application itself exited with an error. A runtime of only 2 seconds means the container is crashing almost immediately — most likely a configuration problem rather than a runtime logic bug.

Step 4: Common root causes and fixes

Missing environment variable or secret: The application crashes immediately because a required config value is absent. The error in logs typically looks like “Required config ‘DATABASE_URL’ is missing” or a NullReferenceException at startup.

# Check which env vars the pod has
kubectl exec -n myapp api-deployment-7d4b9c-xkpq2 -- env | sort
# (If the container won't stay up, use the sleep override technique)

# Verify the secret or ConfigMap exists and has the expected keys
kubectl get secret my-app-secret -n myapp -o jsonpath='{.data}' | base64 -d

# Check if a secret is correctly mounted in the pod spec
kubectl get deployment -n myapp api-deployment -o jsonpath='{.spec.template.spec.containers[0].env}' | python3 -m json.tool

OOMKilled — container exceeding memory limit:

# Increase the memory limit in the deployment
kubectl patch deployment api-deployment -n myapp --type='json' -p='[
  {
    "op": "replace",
    "path": "/spec/template/spec/containers/0/resources/limits/memory",
    "value": "512Mi"
  }
]'

# Or edit the deployment directly
kubectl edit deployment api-deployment -n myapp

Application failing to bind to the expected port: If another process already owns the port (or if the PORT environment variable does not match the containerPort in the spec), the application will exit immediately.

# Verify the containerPort in the deployment matches what the app listens on
kubectl get deployment api-deployment -n myapp -o jsonpath='{.spec.template.spec.containers[0].ports}'

Liveness probe killing the container: If the liveness probe fires too early (before the application has fully started), Kubernetes will restart the pod before it is ready. Increase initialDelaySeconds or switch to a startupProbe.

# Correct liveness probe configuration with adequate startup delay
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 30    # give the app time to start
  periodSeconds: 10
  failureThreshold: 3
startupProbe:                # use startupProbe for slow-starting apps
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30       # allow up to 5 minutes for startup (30 * 10s)
  periodSeconds: 10

Image pull errors vs CrashLoopBackOff

Sometimes the pod status shows ImagePullBackOff or ErrImagePull rather than CrashLoopBackOff. These are distinct — the container is not crashing; Kubernetes cannot even pull the image.

# Check image pull errors
kubectl describe pod -n myapp api-deployment-7d4b9c-xkpq2 | grep -A5 "Events"

# Common fix: verify the imagePullSecret is present and valid in the namespace
kubectl get secret acr-secret -n myapp

# Create the ACR pull secret if missing
az acr credential show --name myRegistry
kubectl create secret docker-registry acr-secret \
  --docker-server=myregistry.azurecr.io \
  --docker-username=myRegistry \
  --docker-password=PASSWORD \
  --namespace myapp

# Better approach: attach ACR to AKS so no pull secret is needed
az aks update \
  --resource-group myRG \
  --name myAKSCluster \
  --attach-acr myRegistry

Advanced: ephemeral debug containers

For crashes that are hard to reproduce or where you need to inspect the filesystem, use kubectl debug to attach an ephemeral container to the running (or failing) pod without modifying the pod spec:

# Attach an ephemeral debug container to inspect a crashing pod
kubectl debug -it -n myapp api-deployment-7d4b9c-xkpq2 \
  --image=busybox \
  --target=api-container \
  --share-processes

# This opens a shell in a busybox container that shares the process namespace
# with the crashing container, allowing you to inspect files, network, and processes

Common mistakes

  1. Reading current logs instead of previous logs. When a container is in CrashLoopBackOff, kubectl logs pod-name shows the logs of the current (about-to-crash or just-started) container, which may be empty or minimal. The crash logs are in the previous container. Always use —previous to read the crash logs.
  2. Setting memory limits too low for JVM or .NET applications. Java and .NET runtimes allocate heap memory based on available system memory if not constrained. In a container with a 256Mi memory limit, the JVM may try to allocate a 128Mi heap plus overhead, exceeding the limit. Set JVM heap explicitly with -Xmx and .NET heap with DOTNET_GCHeapHardLimit environment variable.
  3. Not considering timezone or locale issues at startup. Some applications fail on startup in Azure because they reference a timezone that is not present in the minimal container image. alpine-based images often lack timezone data. Add tzdata package to the container image or use a distro-full base image if your application references timezones at startup.

Frequently asked questions

What does CrashLoopBackOff mean exactly?

CrashLoopBackOff means a container in the pod started, ran briefly, then exited with a non-zero exit code. Kubernetes tried to restart it, it crashed again, and Kubernetes is now backing off (waiting progressively longer intervals before each restart attempt — 10s, 20s, 40s, up to 5 minutes). The "BackOff" part means you can still read the logs from the previous crash attempt with kubectl logs --previous.

How is CrashLoopBackOff different from OOMKilled?

OOMKilled (Out Of Memory Killed) is a specific exit reason where the container exceeded its memory limit and the kernel killed it. It shows up in kubectl describe pod as a reason of OOMKilled in the Last State section. CrashLoopBackOff is the pod phase that describes the restart loop — OOMKilled is one possible reason a container is in that loop. Check kubectl describe pod to see the exit reason behind the crash.

Can I disable CrashLoopBackOff to debug a crashing container?

You cannot disable CrashLoopBackOff, but you can work around it for debugging. Override the container command in the pod spec to run a long sleep: command: ["/bin/sh", "-c", "sleep 3600"]. The container will not crash, giving you time to kubectl exec into it and investigate. Alternatively, use kubectl debug to create an ephemeral debug container attached to the failing pod.

Last verified: 19 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.