Skip to content

ImagePullBackOff

New chapterChapter 36 of 3810 min read

New in this edition, October 2026, for Kubernetes 1.37. Commands checked against the official documentation.

ImagePullBackOff means the kubelet on the node tried to download your container image, failed, and is now waiting before it tries again. The pod has been scheduled; it just can't start because the image never arrived. Run kubectl describe pod <name> and read the Failed event at the bottom: it quotes the error the container runtime got from the registry. Usually that line names the fix on its own: a misspelled tag, a missing pull Secret, a registry the node can't reach. Correct the image reference or the credentials, delete the pod, and the next pull starts straight away.

ErrImagePull first, then the back-off#

You'll see two statuses for the same problem, and they alternate. ErrImagePull is the attempt that just failed. ImagePullBackOff is the waiting period after it. The kubelet's own source sets the first wait at 10 seconds and doubles it after every failure, up to a cap the Kubernetes images documentation gives as 300 seconds (5 minutes). So a pod that's been broken for a while retries roughly every five minutes, and kubectl get pods shows ImagePullBackOff most of the time you look.

That timer is kept per pod and per image. It's the reason waiting is the slow way out: after you fix a Secret, the old pod may sit in back-off for minutes before it notices. A new pod has a new identity and a fresh timer, which is why deleting the pod is the usual last step of every fix below.

A few neighbouring reasons come from the same code and mean something narrower:

  • InvalidImageName: the image string couldn't be parsed at all, so no pull was attempted.
  • ErrImageNeverPull appears when imagePullPolicy: Never is set and the image isn't on that node.
  • FailedToRetrieveImagePullSecret, an event and not a status, says the pod names a pull Secret that doesn't exist in its namespace.

None of these is CrashLoopBackOff. That one means the image pulled fine and the process inside it keeps exiting; the CrashLoopBackOff chapter covers it.

Break a pod on purpose#

You need a running cluster and kubectl pointed at it. A local one is ideal because a few of the checks later run on the node itself: the KinD chapter and the K3s chapter both get you there in a few minutes. If pods themselves are still new, read Kubernetes Pods first.

Start a pod whose tag doesn't exist:

kubectl run typo --image=nginx:no-such-tag --restart=Never
kubectl get pod typo --watch

Within a few seconds the status flips to ErrImagePull, then ImagePullBackOff, then back. Press Ctrl+C and describe it:

kubectl describe pod typo

The Events section at the end of the output looks like this (illustrative; ages and counts will differ on your cluster):

Events:
  Type     Reason   Age                From     Message
  ----     ------   ----               ----     -------
  Normal   Pulling  21s (x2 over 44s)  kubelet  Pulling image "nginx:no-such-tag"
  Warning  Failed   20s (x2 over 43s)  kubelet  Failed to pull image "nginx:no-such-tag": rpc error: code = NotFound desc = failed to pull and unpack image "docker.io/library/nginx:no-such-tag": failed to resolve reference "docker.io/library/nginx:no-such-tag": docker.io/library/nginx:no-such-tag: not found
  Warning  Failed   20s (x2 over 43s)  kubelet  Error: ErrImagePull
  Normal   BackOff  7s (x2 over 42s)   kubelet  Back-off pulling image "nginx:no-such-tag"
  Warning  Failed   7s (x2 over 42s)   kubelet  Error: ImagePullBackOff

Three reasons repeat: Pulling, Failed and BackOff. Only the Failed to pull image line carries information. Everything after the last colon is what the registry answered. Here it's not found, and notice the rewritten reference: you typed nginx, the runtime asked docker.io/library/nginx. A name without a registry host always goes to Docker Hub.

Events expire, though, and on a busy namespace the useful one may already be gone. The pod's status keeps a copy. During back-off the kubelet stores the last real pull error inside the container's waiting message, so this works even when describe shows nothing:

kubectl get pod typo -o jsonpath='{.status.containerStatuses[0].state.waiting.message}{"\n"}'

To follow a pod's events live while you fix things in another terminal, use kubectl events --for pod/typo --watch.

The causes, and how to confirm each#

Match the end of the Failed message to one of the sections below. Each has a check that proves the cause before you edit anything, because a wrong guess (adding a Secret to fix a typo, say) costs you another five-minute back-off.

Wrong name or tag#

The message ends in not found, or in pull access denied, repository does not exist or may require authorization. That second wording trips people up. Docker Hub answers a request for a repository that doesn't exist the same way it answers one for a private repository you can't see, so a typo in the repository name (ngnix) reads like a credentials problem.

Confirm it from your laptop with docker pull and the exact string from the pod spec, copied character for character. If that fails too, the reference is wrong. Look up the real tags on the registry's web page, then fix the manifest. For a Deployment, change the image in your YAML and re-apply, or patch it in place (the container is named app here):

kubectl set image deployment/web app=nginx:1.29

Pinning by digest (image@sha256:…) removes a whole class of these: a digest can't be retagged, so it either exists or it doesn't.

The tag was removed upstream#

A manifest that pulled fine last month can start failing with not found without anyone touching it, because the publisher deleted or moved the tag. The best-known case is Bitnami. In its announcement on GitHub, Bitnami said that from August 28, 2025 existing images, versioned tags included, would move from docker.io/bitnami to a docker.io/bitnamilegacy repository that gets no further updates, and it later postponed deleting the public catalog to September 29. Charts and manifests that still point at an old bitnami/…:x.y tag hit exactly this error. The quick unblock is to repoint at bitnamilegacy; the real fix is choosing a maintained image, since the legacy repository is meant for migration only.

Private registry without a pull Secret#

Messages mentioning authorization failed, no basic auth credentials or pull access denied on an image you know exists mean the node has no credentials for that registry. Kubernetes doesn't use your laptop's docker login; the kubelet needs its own copy, and the usual place for it is a Secret of type kubernetes.io/dockerconfigjson. The private registry task page builds one like this:

kubectl create secret docker-registry regcred \
  --docker-server=<your-registry-server> \
  --docker-username=<your-name> \
  --docker-password=<your-pword> \
  --docker-email=<your-email>

For Docker Hub the server is https://index.docker.io/v1/. Typing a password on the command line leaves it in your shell history; if you've already logged in with Docker, import that file instead:

kubectl create secret generic regcred \
  --from-file=.dockerconfigjson=<path/to/.docker/config.json> \
  --type=kubernetes.io/dockerconfigjson

Then reference it from the pod spec:

apiVersion: v1
kind: Pod
metadata:
  name: private-reg
spec:
  containers:
  - name: private-reg-container
    image: <your-private-image>
  imagePullSecrets:
  - name: regcred

The Secret has to live in the same namespace as the pod. The usual story behind "but I created the Secret": it went into default and the workload runs somewhere else (the Namespaces chapter explains the boundary). If the pod's events show FailedToRetrieveImagePullSecret, the name or namespace is wrong. If the Secret is found and the pull still fails, decode it and check the server and user it actually holds:

kubectl get secret regcred --output="jsonpath={.data.\.dockerconfigjson}" | base64 --decode

Adding imagePullSecrets to every pod gets tedious. Attach the Secret to the namespace's ServiceAccount once and every new pod using that account picks it up automatically, as the ServiceAccount task page shows:

kubectl patch serviceaccount default -p '{"imagePullSecrets": [{"name": "regcred"}]}'

Pods created before the patch don't change. Delete them so their replacements are admitted with the field set.

One change from older clusters is worth knowing. Since Kubernetes 1.35 the KubeletEnsureSecretPulledImages feature has been beta and on by default: a private image the kubelet pulled earlier is no longer free for any pod on that node to use. A pod has to present credentials that successfully pulled that image before, or the kubelet pulls again with whatever the pod brings. A workload that only ran because another pod had warmed the cache can start failing with an auth error after an upgrade, and the fix is the same Secret it should have had all along.

Docker Hub pull limits#

Docker Hub caps pulls per six hours. Its usage and limits page lists 100 pulls for unauthenticated users, counted per IPv4 address or IPv6 /64 subnet, and 200 for an authenticated Personal account; Pro, Team and Business accounts are unlimited. A cluster whose nodes leave through one NAT address shares that single unauthenticated budget, and a rollout across many nodes can spend it in minutes.

When the pull limit is the cause, the error is a long message that links to Docker's documentation. A bare 429 Too Many Requests is the separate abuse limit instead. You can ask the registry what's left from any machine on the same network; a HEAD request returns the headers without counting as a pull:

TOKEN=$(curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq -r .token)
curl --head -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest

Look for ratelimit-limit and ratelimit-remaining in the response; a value like 100;w=21600 is 100 pulls per 21,600 seconds. Pulling with credentials, even a free account's, through an image pull Secret doubles the allowance and counts it per account. A pull-through cache or registry mirror in front of Docker Hub means each image crosses the limit once.

The node can't reach the registry#

Timeouts, no such host, connection refused or TLS certificate errors point at the network between the node and the registry: a firewall, a DNS server that can't resolve the registry host, a corporate proxy the container runtime doesn't know about, or a self-signed certificate on an internal registry. Test from the node, as the next section shows, because your laptop isn't the machine doing the pull. If the same crictl pull fails there, Kubernetes isn't the layer to fix: the runtime's proxy settings, the node's resolver or its trusted certificates are.

Wrong CPU architecture#

The image exists, the credentials work, and the pull still fails because the image has no build for the node's platform. Under containerd, the message contains no match for platform in manifest: not found; Docker's own client says no matching manifest for, then the platform it wanted (an arm64 node asks for linux/arm64), then in the manifest list entries. Check both sides:

kubectl get nodes -L kubernetes.io/arch
docker buildx imagetools inspect nginx:1.29

The first prints each node's architecture from the label the kubelet sets. The second lists a Platform: line for every build the image offers. If the node's architecture is missing from that list, rebuild the image for it (docker buildx build --platform linux/amd64,linux/arm64 . produces both) or keep the pod off those nodes with a nodeSelector on kubernetes.io/arch.

imagePullPolicy and :latest#

The policy decides whether the kubelet goes to the registry at all. IfNotPresent pulls only when the image is missing from the node. With Always, the kubelet asks the registry on every container start, which is cheap because cached layers aren't downloaded twice. Never uses whatever is on the node and fails if it's absent.

If you leave the field out, the cluster fills it in when the object is created. A :latest tag or no tag at all gets Always; any other tag, or a digest, gets IfNotPresent. That value is then frozen. Changing a Deployment's image from :1.4 to :latest later doesn't flip the policy to Always; you have to change it yourself. It's also behind the local-image trap on kind, below.

Pull from the node yourself#

When the events aren't enough, repeat the pull where the kubelet does it. crictl talks to the same container runtime the kubelet uses, so its result is the real answer for that node. Find the node first:

kubectl get pod typo -o wide

On a kind cluster each node is a Docker container, and crictl is already inside it:

docker exec -it kind-control-plane crictl pull nginx:no-such-tag
docker exec -it kind-control-plane crictl images

K3s ships its own copy as sudo k3s crictl. On a node you've reached over SSH it's just sudo crictl pull <image>, and for a private registry add --creds USERNAME:PASSWORD to test the same credentials your Secret holds. A pull that succeeds here while the pod still fails points back at the pod: its Secret, its namespace, or its pull policy.

Local images on kind and k3s#

You built my-app with Docker on your machine, deployed it, and got ImagePullBackOff. The cluster nodes have their own image store, separate from your Docker daemon, so the image isn't there. Then the kubelet looks for it on Docker Hub under docker.io/library/my-app, which doesn't exist.

For kind, load the image into the nodes, as described in the kind quick start:

docker build -t my-app:dev .
kind load docker-image my-app:dev
kind load docker-image my-app:dev --name test-cluster

The second form is for a cluster you created with a name. Use a real tag here. Loaded as my-app:latest, the image gets the default Always policy, the kubelet goes to the registry anyway, and you're back where you started; the kind docs say to avoid :latest or set imagePullPolicy: IfNotPresent (or Never) on the container.

K3s runs containerd too, and the K3s image import page gives two routes. Drop an image tarball into /var/lib/rancher/k3s/agent/images/ on the node and K3s imports it, at startup or while it's running. Or import it directly with the embedded ctr:

docker save my-app:dev -o my-app.tar
sudo k3s ctr images import my-app.tar
sudo k3s ctr images list

On a multi-node cluster this has to happen on every node that might run the pod, since each one has its own store.

Clean up#

Remove the test pod. If you patched the default ServiceAccount only to try it out, take the field back off:

kubectl delete pod typo
kubectl patch serviceaccount default --type='json' -p='[{"op": "remove", "path": "/imagePullSecrets"}]'
kubectl delete secret regcred

Common questions#

Two that come up after the causes above are fixed.

Does ImagePullBackOff fix itself?#

Sometimes. The kubelet never stops retrying, so a registry outage or an expired rate limit clears on its own within one back-off period after the cause goes away. A typo or a missing Secret never will.

Why does it work on one node and not another?#

Each node pulls and caches on its own. A node that already holds the image under IfNotPresent never contacts the registry, so it hides a broken credential or a deleted tag that a fresh node runs straight into. Architecture and network path can differ per node too. Compare kubectl get pod -o wide across the failing and working replicas, then run crictl pull on one of each.

Next in the book#

If the image now pulls but the container keeps restarting, go to CrashLoopBackOff. To roll a fixed image out to every replica without downtime, the Kubernetes Deployment chapter covers updating a Deployment's image.