
troubleshooting
ImagePullBackOff and ErrImagePull: what they mean and how to fix them
ErrImagePull is the failure. ImagePullBackOff is the waiting. The real error is in the pod's Events, and there are only about seven things it ever says.
These two statuses are the same problem at two different moments. ErrImagePull
is the kubelet telling you a pull just failed. ImagePullBackOff is the kubelet
telling you it has failed enough times that it is now waiting before trying again.
The important consequence: by the time you look, the status has usually changed
from the useful one to the useless one. ImagePullBackOff contains no
information about why. The reason is in the pod’s Events, and that is the only
place it lives.
The one command that matters
kubectl describe pod <pod> | tail -20
The Events section at the bottom carries the registry’s actual response. Read that line before anything else — it names the cause almost every time:
Failed to pull image "myco/api:v1.4.2": rpc error: code = Unknown
desc = failed to pull and unpack image "docker.io/myco/api:v1.4.2":
failed to resolve reference: pull access denied, repository does not
exist or may require authorization
Everything below is a translation table for what that line can say.
“pull access denied, repository does not exist or may require authorization”
This is the most common message and the most misleading one, because it is two completely different problems sharing one string. Registries deliberately refuse to distinguish “this does not exist” from “you cannot see it” — telling anonymous users which private repositories exist is an information leak.
So check them in this order, cheapest first:
- Is the name right? Typos in the repository or tag produce exactly this.
Verify the tag exists at all, from your laptop where you are authenticated:
docker manifest inspect myco/api:v1.4.2 - Does the pod have a pull secret?
kubectl get pod <pod> -o jsonpath='{.spec.imagePullSecrets}' - Is that secret in the right namespace? This is the single most common real cause, covered below.
Pull secrets are namespaced, and that catches everyone
imagePullSecrets references a Secret in the pod’s own namespace. A secret
called regcred in default does nothing for a pod in production, and nothing
warns you — the reference to a non-existent secret is silently ignored, and you
get the generic access-denied message.
kubectl get secret regcred -n <the-pod's-namespace>
If it is missing, copy or recreate it there:
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=<user> \
--docker-password=<token> \
-n production
Rather than adding imagePullSecrets to every manifest, attach it once to the
namespace’s default ServiceAccount and every pod in that namespace inherits it:
kubectl patch serviceaccount default -n production \
-p '{"imagePullSecrets":[{"name":"regcred"}]}'
Existing pods are unaffected — the ServiceAccount’s secrets are applied at pod creation, so you need to recreate the pods for it to take effect.
“toomanyrequests: You have reached your pull rate limit”
Docker Hub rate-limits anonymous pulls at 100 per 6 hours, counted per IPv4 address or IPv6 /64 subnet (Docker’s current published limits), and authenticated Personal accounts at 200 per 6 hours. Pro, Team and Business are unlimited.
Two things make this bite harder than the numbers suggest:
- The limit is per IP, so an entire cluster behind one NAT gateway shares one anonymous budget. A twenty-node cluster exhausts it quickly during a rollout.
- A multi-arch image counts as one pull per architecture, and version checks are free but downloads are not.
Fix it by authenticating even for public images — an authenticated free account doubles the budget and stops the limit being shared with everyone else on your NAT address. The durable fix is a pull-through cache or registry mirror so repeated pulls of the same image never leave your network.
Note that plenty of blog posts still quote “10 pulls per hour” for anonymous users. That was announced in early 2025 and then walked back; the table linked above is the current one.
“no match for platform in manifest”
The image exists and you can read it, but it was not built for your nodes'
architecture. This became common the moment Apple Silicon became the standard
development machine: docker build on an M-series Mac produces an arm64 image,
and it will not run on amd64 nodes.
docker buildx build --platform linux/amd64,linux/arm64 -t myco/api:v1.4.2 --push .
Check what a tag actually contains with docker manifest inspect <image> and look
at the platform entries.
“x509: certificate signed by unknown authority”
The node does not trust your registry’s certificate. This is a node-level
problem, not a Kubernetes one — the kubelet’s container runtime does the pulling,
so the CA has to be installed on the node’s trust store or in the runtime’s
configuration (/etc/containerd/certs.d/<registry>/hosts.toml for containerd).
Nothing you put in a manifest will fix it.
The statuses that look similar but are not
| Status | What it actually means |
|---|---|
ErrImageNeverPull | imagePullPolicy: Never and the image is not already on the node. Nothing was attempted. |
InvalidImageName | The reference is malformed — usually an unresolved template variable, a stray space, or a capital letter in the repository name. Never reaches the registry. |
ImageInspectError | The image was pulled but the runtime cannot read it. Corrupt layer or a disk problem on the node. |
CreateContainerConfigError | The image is fine. A referenced ConfigMap or Secret is missing. |
InvalidImageName is worth recognising on sight, because it means no network call
happened at all and you can stop investigating the registry.
The pull policy trap: “I pushed a fix and nothing changed”
imagePullPolicy defaults are derived from the tag, and this surprises people:
- Tag is
:latest, or omitted entirely →Always - Any other tag →
IfNotPresent
So if you push a new image over an existing tag like :v1.4.2, nodes that already
have that tag cached will never fetch it. The pod starts happily and runs your old
code, which is a worse outcome than a failed pull because nothing looks wrong.
Use immutable tags, or pin by digest, which removes the ambiguity entirely:
image: myco/api@sha256:9f2b8a... # cannot be repointed
Why the wait keeps getting longer
The pull backoff is the same exponential schedule as a crash loop: 10s, 20s, 40s,
80s, 160s, then capped at 5 minutes. So after you fix a pull secret, the pod can
sit in ImagePullBackOff for another five minutes looking exactly as broken as it
was before.
It has not ignored your fix. It is waiting. To get the answer immediately, delete
the pod and let the controller create a fresh one with a clean backoff — the same
reason a CrashLoopBackOff seems to linger after a fix, covered in
how to fix CrashLoopBackOff.
Confirming the fix from the node’s point of view
The kubelet pulls, not you, so a successful docker pull on your laptop proves
very little. To test with the node’s credentials and network:
kubectl debug node/<node> -it --image=busybox
Or, if you have shell access to the node itself:
crictl pull myco/api:v1.4.2
That distinguishes a credentials problem from a network or DNS problem in one step, which reading Events cannot always do.
Stopping it recurring
- Pin by digest for production, or at minimum never overwrite a tag.
- Put pull secrets on the ServiceAccount, not in every deployment manifest.
- Run a pull-through cache for upstream images. It removes rate limits, survives registry outages, and makes rollouts faster.
- Build multi-arch, or build on the target architecture. Mixed-architecture clusters catch this at the worst time.
- Alert on pods pending longer than a few minutes. An image that cannot be pulled during a rollout will not take your service down — the old ReplicaSet keeps serving — so nothing pages you while the deployment quietly stalls.
That last one is the real risk with pull failures. Unlike a crash loop, they often fail invisibly: the rollout stops, the old pods carry on, and you find out days later that the fix you shipped on Tuesday never actually went out. Both statuses are also computed the same client-side way as everything else in the STATUS column, which is worth understanding if you are ever unsure which container the status refers to — see how kubectl decides what to put in the STATUS column.
KubeGlance shows pod events and pull errors next to the workload they belong to — on the Mac, and natively on iPhone and iPad when the alert catches you away from your desk.
Get KubeGlanceThe Kubernetes dashboard that fits in your pocket
KubeGlance is a native Kubernetes client for iPhone and iPad — the real dashboard, not a companion — with a full Mac app on the same core. Pods, workloads, logs and events, straight from your kubeconfig.
Download KubeGlance

