A pod stuck in ImagePullBackOff looks like this:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
web-7d9c4b8f5c-x2klm 0/1 ImagePullBackOff 0 2m
ImagePullBackOff is not the error — it is the kubelet's response to an error. It tried to pull your image, the pull failed, and it is now waiting before retrying, with the delay growing after each attempt up to a five-minute cap. The status you often see first, for a few seconds, is ErrImagePull; once backoff begins it becomes ImagePullBackOff.
The actual reason is in the pod's events, and reading it takes one command:
kubectl describe pod web-7d9c4b8f5c-x2klm
Scroll to the bottom:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 2m default-scheduler Successfully assigned default/web-7d9c4b8f5c-x2klm to node-1
Normal Pulling 2m (x3 over 2m) kubelet Pulling image "myrepo/web:v1.2.3"
Warning Failed 2m (x3 over 2m) kubelet Failed to pull image "myrepo/web:v1.2.3": rpc error: code = NotFound desc = failed to pull and unpack image "docker.io/myrepo/web:v1.2.3": failed to resolve reference: docker.io/myrepo/web:v1.2.3: not found
Warning Failed 2m (x3 over 2m) kubelet Error: ErrImagePull
Normal BackOff 1m (x6 over 2m) kubelet Back-off pulling image "myrepo/web:v1.2.3"
Warning Failed 1m (x6 over 2m) kubelet Error: ImagePullBackOff
The Failed event's message is the one that matters. Everything below is chosen by what it says.
Match the Message to the Cause
| Message contains | Cause | Fix |
|---|---|---|
not found, manifest unknown | Wrong image name, tag, or registry path | Fix 1 |
unauthorized, authentication required | Missing or wrong pull secret | Fix 2 |
no such host, dial tcp, i/o timeout | Node cannot reach the registry | Fix 3 |
toomanyrequests, pull rate limit | Docker Hub rate limit | Fix 4 |
InvalidImageName as the status | Malformed reference, not a pull failure | Fix 5 |
Fix 1: Wrong Image Name or Tag
By far the most common cause, and almost always a typo or a tag that was never pushed.
Check exactly what the spec asks for:
kubectl get pod web-7d9c4b8f5c-x2klm -o jsonpath='{.spec.containers[*].image}'
Then verify that reference exists from a machine with access:
docker manifest inspect myrepo/web:v1.2.3
If that fails the same way, the image genuinely is not there. Common variations:
- The tag was never pushed — CI built
v1.2.3but the push step failed. - The registry host is missing, so
myregistry.io/myrepo/webwas written asmyrepo/weband resolved against Docker Hub. - Case matters.
MyRepo/Webis notmyrepo/web.
Correct it in the deployment:
kubectl set image deployment/web web=myrepo/web:v1.2.4
Fix 2: Private Registry Without Credentials
An unauthorized message means the registry answered and refused you. The node needs credentials.
Create the secret in the same namespace as the pod:
kubectl create secret docker-registry regcred \
--docker-server=myregistry.io \
--docker-username=<user> \
--docker-password=<token> \
--namespace=default
Reference it in the pod spec:
spec:
imagePullSecrets:
- name: regcred
containers:
- name: web
image: myregistry.io/myrepo/web:v1.2.3
Two things trip people up here repeatedly:
Namespace scoping. Secrets are namespaced. A regcred in default does nothing for a pod in staging — you need one per namespace.
--docker-server must match the registry host in the image reference exactly. For Docker Hub the correct value is https://index.docker.io/v1/, not docker.io, and a mismatch means the credential is simply never applied.
To attach the secret to every pod in a namespace without editing each spec, patch the service account:
kubectl patch serviceaccount default -n default \
-p '{"imagePullSecrets": [{"name": "regcred"}]}'
Use a read-only, pull-scoped token here rather than an account password. The secret is stored base64-encoded, not encrypted, so anyone who can read secrets in that namespace can read the credential.
Fix 3: The Node Cannot Reach the Registry
no such host or i/o timeout means this is a network problem, not an authentication one. The registry name never resolved, or the connection never completed.
Test from a pod on the affected node:
kubectl run nettest --rm -it --image=busybox --restart=Never -- \
sh -c "nslookup myregistry.io && wget -S --spider https://myregistry.io/v2/"
Usual causes: a private registry only reachable inside a VPC that the node pool is not in; a NetworkPolicy blocking egress; a proxy the container runtime is not configured to use; or an internal registry using a certificate the node does not trust. Note that proxy settings for pulls belong to the container runtime on the node (containerd or CRI-O), not to your pod's environment variables — setting HTTPS_PROXY in the pod spec does nothing for image pulls.
Fix 4: Docker Hub Rate Limit
toomanyrequests: You have reached your pull rate limit.
Anonymous pulls from Docker Hub are limited per source IP. Every node behind one NAT gateway shares that budget, so a cluster hits the ceiling far faster than a laptop, and it typically appears as a sudden failure across many pods at once.
Two durable fixes: authenticate, using a pull secret as in Fix 2 even for public images, which raises the limit and attributes pulls to an account; or mirror the images you depend on into your own registry and reference them there, which also removes an external dependency from your deploys.
Fix 5: InvalidImageName
If the status is InvalidImageName rather than ImagePullBackOff, no pull was ever attempted — the reference itself is malformed and the kubelet rejected it. Look for an unsubstituted template variable (image: ${IMAGE} that Helm never filled in), a stray space, or a double colon. Print the resolved manifest to see what was actually applied:
kubectl get deployment web -o yaml | grep -A2 'image:'
Verify the Fix
After correcting the image or the secret, watch the pod recover:
kubectl get pods -w
You are looking for ImagePullBackOff → ContainerCreating → Running.
Changing the image in the deployment triggers a new rollout automatically. Creating or fixing a secret does not — the existing pod keeps backing off with its old failure. Force a retry by deleting the pod, which the controller recreates:
kubectl delete pod web-7d9c4b8f5c-x2klm
Or restart the whole deployment:
kubectl rollout restart deployment/web
kubectl rollout status deployment/web
Confirm the image the running pod actually landed on:
kubectl get pod -l app=web -o jsonpath='{.items[0].status.containerStatuses[0].image}'
Prevention
- Pin tags or digests, never
latest. A digest (web@sha256:...) is immutable and makes the pull reproducible;latestalso forcesimagePullPolicy: Always, so every registry hiccup becomes a failed deploy. - Verify the push before you deploy. A CI pipeline that deploys after a push step whose failure it ignores produces this error on a schedule.
- Create pull secrets in every namespace you deploy to, or attach them to the service account once.
- Mirror third-party images into your own registry so an upstream rate limit or deletion cannot break your deploys.
- Alert on pods not Ready.
ImagePullBackOffretries forever and never fails the deployment on its own, so without an alert a broken rollout can sit unnoticed indefinitely.