I broke four pods on Azure Kubernetes Service on purpose. Two said ImagePullBackOff, one said CreateContainerConfigError, and one crashed four times in a minute and a half. Every time the question was the same: which command shows you why?
This explainer follows the line every pod walks before your code runs, shows where each status stops it, and which command finds the reason at each stop, with the real output from my own cluster.
Key points
- The status names the stop. The kubelet pulls the image, fills in the container's settings (environment variables and Secrets), then starts the container. ImagePullBackOff stops at the first, CreateContainerConfigError at the second, CrashLoopBackOff after the third: your app ran and died.
- BackOff means waiting, not giving up. Kubernetes keeps trying, and the wait between two tries grows up to five minutes. A fix (a pushed tag, registry access, a created Secret) is picked up on the next try, without a redeploy.
- Before your app runs, only Kubernetes knows the reason.
kubectl describe podends with the events. Read the first error: a wrong tag also prints401 Unauthorized, because AKS always tries an anonymous pull as well. - After your app ran, its own logs have the answer. In a crash loop, plain
kubectl logsshows the container that just crashed;--previousonly helps once the container runs again. The RESTARTS column picks the command: zero and not running → describe, more than zero → logs.
Stuck at the image
A pod is the small wrapper Kubernetes runs your container in. Before your code runs, the kubelet, the agent on every machine of the cluster, pulls the container image from the registry, fills in the container's settings, and starts the container. The status in kubectl get pods names the stop where the pod is waiting.
My first break was a typo: the tag v1.0, the tag in the Microsoft Learn module's example, while my registry only had v1. A small script of mine printed every status change with the second it happened, like kubectl get pods --watch with timestamps:
> python watch.py pods api-typo --secs 750.4s api-typo-… ContainerCreating 0/11.1s api-typo-… ErrImagePull 0/116.4s api-typo-… ImagePullBackOff 0/127.4s api-typo-… ErrImagePull 0/139.0s api-typo-… ImagePullBackOff 0/1
ErrImagePull means a try just failed; ImagePullBackOff means Kubernetes is waiting before the next try, and each wait is longer than the last (up to five minutes). Nine and a half minutes later the pod still said ImagePullBackOff, with zero restarts. The tries never stop.
Your app never ran, so it has nothing to say. Kubernetes does: kubectl describe pod ends with the events, what the kubelet did and what failed:
> kubectl describe pod -l app=api-typoStatus: PendingReason: ImagePullBackOffRestart Count: 0Failed to pull image "…/aks-api:v1.0": … not found, … 401 Unauthorized
Unauthorized doesn't mean permissions here. The first error is the real one: not found. AKS always tries a pull without credentials as well, and that try gets the 401 (Microsoft's troubleshooting guide says so for wrong image names). Check the image name and the tag against your registry; fix the tag in your YAML and apply it again, or push the missing tag, which Kubernetes picks up on its next try.
My second break used the same image in a second registry the cluster was never given access to. Now the event had the 401 and nothing else:
> kubectl describe pod -l app=api-privateReason: ErrImagePullRestart Count: 0Failed to pull image "otherregistry…": … 401 Unauthorized
The module calls registry access "usually automatic". It isn't: the cluster pulls images with its own identity (the kubelet's managed identity), and that identity needs the AcrPull role on each registry. My first registry had it only because I attached it when I created the cluster. For an existing cluster:
az aks update -g rg-demo -n aks-demo --attach-acr otherregistry
That took 106 seconds. I didn't redeploy and didn't touch the pod; 32 seconds after I started watching, the app was running.
Stuck at the settings
The module's manifest reads an API key from a Secret, a Kubernetes object that holds keys and passwords:
env:- name: MODEL_NAMEvalue: "gpt-5-mini"- name: API_KEYvalueFrom:secretKeyRef:name: api-secrets # never createdkey: api-key
The module lists Secrets under CrashLoopBackOff, as if a missing one makes the app crash. It doesn't: the kubelet fills in the settings before the container exists, and a required Secret that's missing stops it right there. The status was CreateContainerConfigError after half a second, and then nothing: no crash, no restart. The container was never created, so there are no logs:
> kubectl logs deploy/api-secretError from server (BadRequest): container "api" in pod "…"is waiting to start: CreateContainerConfigError
The reason is in the events again:
> kubectl describe pod -l app=api-secretReason: CreateContainerConfigErrorRestart Count: 0API_KEY: <set to the key 'api-key' in secret 'api-secrets'>Failed (x5 over 42s) Error: secret "api-secrets" not found
The fix is the Secret itself (kubectl create secret generic api-secrets --from-literal=api-key=demo). Nine seconds later the same pod was running, without a redeploy or a restart.
The app ran, and died
My app needs one setting, MODEL_NAME; without it, it prints an error and exits with code 1. I deployed it without that setting and watched it for a hundred seconds:
> python watch.py pods api-crash --secs 1001.2s api-crash-… Running 1/11.9s api-crash-… Error 0/12.6s api-crash-… Running 1/1 restarts 14.1s api-crash-… Error 0/1 restarts 118.0s api-crash-… CrashLoopBackOff 0/1 restarts 194.2s api-crash-… Running 1/1 restarts 4
This time the container did start, so Kubernetes restarts it: the first restart comes at once, then the wait doubles (10 s, 20 s, 40 s, up to five minutes; it resets after ten minutes without problems). My restarts came a little later than that, but further and further apart. CrashLoopBackOff means Kubernetes is sitting out one of those waits.
This time your app ran, so it has something to say:
> kubectl logs deploy/api-crasherror: MODEL_NAME is not set
Most guides, the module too, say: for a crash loop, ask for the previous container's logs. Right after the plain logs, that gave me an error of its own:
> kubectl logs deploy/api-crash --previousunable to retrieve container logs for containerd://74a2e527a39d…
kubectl describe pod shows why: the pod had two terminated containers, the current one and the one before.
State: TerminatedExit Code: 1Last State: TerminatedExit Code: 1Restart Count: 4
While a crashed container waits for its restart, it is still the pod's latest container, so plain kubectl logs shows it. --previous asks for the one before, and the kubelet had already cleaned that one up (it garbage-collects dead containers). Use --previous once the container is running again, when the crash is behind it.
The one column that picks the command
kubectl get pods has a RESTARTS column. At the end of my run:
> kubectl get podsNAME READY STATUS RESTARTS AGEapi-crash-565c6974d4-r5xzq 0/1 Error 4 (116s ago) 2m37s
The pods that never got to run their app had zero restarts in every check. Zero restarts and not running: your app never ran, so ask Kubernetes with kubectl describe pod. More than zero: it ran and died, so ask your app with kubectl logs.
| Status | Where it stops | Restarts | Read |
|---|---|---|---|
| ErrImagePull / ImagePullBackOff | pulling the image | 0 | describe → events (the first error) |
| CreateContainerConfigError | filling in the settings (a missing Secret or ConfigMap) | 0 | describe → events |
| CrashLoopBackOff | after the start: the app exited | > 0 | logs (plain first, --previous once it runs again) |
What happened when I ran it
Tested on October 26, 2026 in my own Azure subscription (AKS 1.36 in westeurope, Azure CLI 2.88.0, kubectl 1.36); everything was deleted afterwards. One AKS cluster served four videos of this series; this page uses its pod status steps. The cluster had one Standard_D2s_v5 node and the free tier; the app is a small Python API shaped like the module exercise's (port 8000, MODEL_NAME required), without the exercise's model in Microsoft Foundry.
- During the attach of the second registry, Kubernetes replaced the waiting pod with a new one of the same ReplicaSet (a new pod name; the run doesn't show why). No redeploy was needed either way: the app ran 31.7 seconds after the watcher started.
- My first try of the crash and Secret steps never got that far: both pods stayed Pending with
0/1 nodes are available: 1 Insufficient cpu, because my own earlier deployments had filled the node's CPU requests. That's the next video's topic (requests and limits); after cleaning up, the rerun showed the statuses above. - In the crash loop the restarts came at 2.6, 18.7, 40.4 and 94.2 seconds of the watcher.
What the video leaves out
- Pending pods and
Insufficient cpu(requests and limits: the next video). - Image pull secrets (
imagePullSecrets), ABAC-enabled registries (where--attach-acrdoesn't assign the right role) and image architecture mismatches, which also show401 Unauthorized(Microsoft's troubleshooting guide covers them). - Liveness probes as a cause of restarts,
kubectl get eventsandkubectl debug.



