ImagePullBackOff and ErrImagePull: the seven causes and how to tell them apart

Here the application did not fail: it never started. Kubernetes could not pull the image, which is why searching the logs is useless — there are no logs, because there is no container. The message you need is in the pod events.

ErrImagePull and ImagePullBackOff are the same problem at two moments

`ErrImagePull` is the first attempt failing. `ImagePullBackOff` is Kubernetes waiting before trying again, with the wait growing each round.

If you see the two states alternating in `kubectl get pods`, they are not two problems: it is one retrying. The cause is in a single place.

And unlike CrashLoopBackOff, `kubectl logs` will give you nothing useful. The container does not exist yet. Confusing the two states is what costs you the first half hour.

The command that gives the answer

`kubectl describe pod <name>` and scroll to the Events section. That is where the literal message the registry returned lives, and that message distinguishes between every cause below.

"manifest unknown" or "not found" is a tag that does not exist. "unauthorized" or "authentication required" is credentials. "toomanyrequests" is a rate limit. "no match for platform" is architecture.

It is worth reading the full message before theorising: the registry already said what is wrong, and it is almost always literal.

The causes, most frequent first

**The tag does not exist.** A typo, or `:latest` pointing at something that was deleted, or a tag the pipeline never published because the build failed and nobody checked. Verify from your machine with `docker manifest inspect <image>:<tag>`.

**The imagePullSecret is missing.** The registry is private and the pod has no credentials. It shows as `unauthorized`. Watch one detail: the secret must be in the **same namespace** as the pod. Copying a Deployment to a new namespace and forgetting the secret is the most common version of this.

**Public registry rate limit.** Docker Hub limits anonymous pulls per IP address, and in Kubernetes every node behind the same NAT shares one. A mid-size cluster hits it without doing anything unusual. It shows as `toomanyrequests`. The fix is authenticating even for public images, or using an internal mirror.

**Wrong architecture.** Since arm64 nodes started sitting next to amd64 ones — Graviton on AWS, Axion on GCP — an image built only for amd64 fails on the arm node with `no match for platform`. What makes it baffling is that it works on some nodes and not others, which looks intermittent and is not.

**Expired credentials.** Registry tokens expire. A pod that ran for months fails when rescheduled, because the token was renewed in the pipeline but not in the cluster secret.

**The registry is not answering.** A provider outage, or a network rule blocking egress from the nodes. You can tell because it fails for every image from that registry at once.

**The image is huge and the attempt times out.** Uncommon, but it happens with multi-gigabyte images on nodes with slow disks or saturated networks.

Why `:latest` makes all of this worse

With a moving tag like `:latest`, the image pulled today is not the one pulled yesterday, and no record says which was which. A rescheduled pod can bring a different version from its siblings, and then the same Deployment runs two codebases at once.

Worse: when it fails you cannot know which image was running before, because the tag now points somewhere else. Rollback loses its reference.

Pinning by digest — `image@sha256:...` — or at least by an immutable version turns this into a reproducible problem. It is the practice that avoids the most pain for the least work.

If it worked before, something changed — and here it almost certainly did

In this failure the change is nearly the definition of the problem. A new tag the pipeline did not publish, a rotated secret, a new namespace without credentials, an arm node added by the autoscaler to a cluster that was all amd64 until yesterday.

That last case is especially slippery with autoscalers that pick instance types by price, like Karpenter: the cluster can start receiving nodes of an architecture nobody explicitly asked for, and single-arch images begin failing in an apparently random way.

The data that settles it is which exact image was active before the first failure, and which node the working pods are running on.

How Moonin shortens that part

Moonin keeps a revision history per service with the exact image and commit, so the image that was active before the failure is a fact rather than pipeline archaeology.

And it keeps an inventory of cluster resources, nodes included with their characteristics, so a cluster with mixed architectures looks like one instead of being discovered when something breaks.

What it does not do: it does not fix manifests, does not create secrets and does not retry pulls. It tells you which image changed and when; the fix is yours.

See the history of your own cluster

The demo is open and needs no form: you can look at how the image history per service and the node inventory appear, before installing anything.

About usPricingSecurity and permissionsPartner programmevs Datadogvs Prometheus & Grafanavs New Relicvs Dynatracevs ELK and ElasticsearchCrashLoopBackOffOOMKilledImagePullBackOffPod stuck in PendingDocumentationDemoSign upContact usArguz, the consultancy

Frequently asked questions

Why does kubectl logs show nothing?

Because the container never started: Kubernetes could not pull the image, so no process ever wrote logs. The message you need is in `kubectl describe pod <name>`, in the Events section, where the registry states literally what failed.

What is the difference between ErrImagePull and ImagePullBackOff?

The same problem at two moments. ErrImagePull is the attempt failing; ImagePullBackOff is Kubernetes waiting before retrying, with the wait growing each time. If you see them alternating, they are not two different causes.

Why do I get toomanyrequests if the image is public?

Because Docker Hub limits anonymous pulls per IP address, and in Kubernetes every node behind the same NAT shares one. A mid-size cluster hits the limit without doing anything unusual. Authenticating even for public images raises the limit, and an internal mirror removes it.

It works on some nodes and fails on others, why?

Almost always architecture. If you have arm64 nodes alongside amd64 ones — Graviton on AWS, Axion on GCP — an image built only for amd64 fails on the arm nodes with `no match for platform`. It looks intermittent but it is deterministic: it depends which node the pod landed on. The fix is publishing the image for both architectures.

Why does my pod fail only in a new namespace?

Because imagePullSecrets live in the namespace. Copying a Deployment to another namespace without copying the secret produces exactly this error, with an `unauthorized` message, even though the same Deployment works perfectly in the original namespace.