KubeGlanceDownload
ImagePullBackOff and ErrImagePull: what they mean and how to fix them

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.

· 8 min read

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 two statuses are one loop. ErrImagePull carries the message; ImagePullBackOff is just the wait between attempts.

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:

  1. 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
  2. Does the pod have a pull secret? kubectl get pod <pod> -o jsonpath='{.spec.imagePullSecrets}'
  3. 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

StatusWhat it actually means
ErrImageNeverPullimagePullPolicy: Never and the image is not already on the node. Nothing was attempted.
InvalidImageNameThe reference is malformed — usually an unresolved template variable, a stray space, or a capital letter in the repository name. Never reaches the registry.
ImageInspectErrorThe image was pulled but the runtime cannot read it. Corrupt layer or a disk problem on the node.
CreateContainerConfigErrorThe 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 KubeGlance
#imagepullbackoff #errimagepull #registry #kubectl #troubleshooting

The 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