How to Fix ImagePullBackOff and ErrImagePull in Kubernetes (2026)

Oct 20, 2026 · 13 min · Ajay Kumar

This guide is for anyone staring at a pod stuck in ImagePullBackOff or ErrImagePull who wants the cause, not a list of guesses. By the end you will know how to read the one line in the pod events that names the cause, and you will have a fix for each of the seven causes I see most: wrong names, moved Bitnami images, private registry credentials, cloud registry IAM, Docker Hub rate limits, CPU architecture and network. Commands match Kubernetes 1.34 to 1.37 (1.37.1 is current) with containerd, plus AWS CLI v2, gcloud and Docker 28. You need kubectl access to the namespace and, for the cloud sections, permission to read node IAM settings.

Step 1: Read the real error message from pod events

ErrImagePull is the first failure. ImagePullBackOff is the kubelet waiting before it tries again, with a delay that grows until it hits a compiled-in limit of 300 seconds. Neither status tells you why. The event message does.

# Which container is failing, and on which node
kubectl get pod my-app-7c9f8d6b5-x2lqk -o wide

# The Events section at the bottom has the registry's actual answer
kubectl describe pod my-app-7c9f8d6b5-x2lqk | sed -n '/Events:/,$p'

# Or every pull failure in the namespace, newest last
kubectl get events --field-selector reason=Failed --sort-by=.lastTimestamp

You are looking for the text after Failed to pull image "...". Match it to a section below:

Message contains Go to
not found, manifest unknown Cause 1 or 2
401 Unauthorized, 403 Forbidden, authorization failed, no basic auth credentials Cause 3 or 4
429 Too Many Requests, toomanyrequests, pull rate limit Cause 5
no match for platform in manifest Cause 6
i/o timeout, server misbehaving, context canceled Cause 7

To reproduce a pull outside the cluster without needing the node, query the registry directly. docker buildx imagetools inspect reads the manifest without downloading layers:

docker buildx imagetools inspect docker.io/library/redis:8.2.1
Pod in ImagePullBackOff kubectl describe pod, read Events What does the event message say? text after Failed to pull image not found manifest unknown 401 / 403 unauthorized 429 pull rate limit no match for platform in manifest i/o timeout context canceled Fix registry, repo, tag Bitnami: tag moved to bitnamilegacy imagePullSecrets in same namespace, or node IAM for ECR/GAR Log in to Docker Hub or mirror via a pull-through cache Build multi-arch with buildx, or pin pods with nodeSelector Check node DNS, NAT, proxy, VPC endpoints, image size Kubelet retries with growing backoff up to 300 s; fix the cause, then delete the pod to retry now
Decision flow from the event message to the fix. The 300-second backoff ceiling is from the Kubernetes images documentation.

Cause 1: Wrong image name, tag or registry

This is still the most common cause. Two defaults catch people: an image with no registry means docker.io, and an image with no tag means :latest, per the Kubernetes image naming rules. So image: myorg/api is really docker.io/myorg/api:latest, which may not exist if you push to GHCR or ECR. Here is what a missing tag looks like from the registry, run on 20 October 2026:

$ docker pull registry.k8s.io/pause:does-not-exist
Error response from daemon: failed to resolve reference "registry.k8s.io/pause:does-not-exist": registry.k8s.io/pause:does-not-exist: not found

On a node the event wording is containerd's, but it ends in the same not found. Fix it by copying the exact reference from your registry or CI output, and prefer digests in production manifests so that a retagged image cannot surprise you:

# deployment.yaml (excerpt)
spec:
  template:
    spec:
      containers:
        - name: api
          # Full registry path, explicit tag, and digest. Only the digest is used to pull.
          image: ghcr.io/myorg/api:1.8.3@sha256:<digest-from-ci>
          imagePullPolicy: IfNotPresent

Also check the image was actually pushed for this commit. A failed CI push step plus a deploy that runs anyway produces exactly this error.

Cause 2: Bitnami images that moved in 2025

If the image starts with docker.io/bitnami/ and the tag is a version, it probably no longer exists there. On 28 August 2025 Bitnami moved versioned tags to docker.io/bitnamilegacy, which gets no updates, and kept only a small hardened set of latest tags for free. The packaged charts were left as they were. As of today the bitnami/redis chart's values.yaml in the charts repository still defaults to bitnami/redis:8.2.1-debian-12-r0. I checked both repositories:

$ curl -s -o /dev/null -w "%{http_code}\n" \
    https://hub.docker.com/v2/repositories/bitnami/redis/tags/8.2.1-debian-12-r0
404
$ curl -s -o /dev/null -w "%{http_code}\n" \
    https://hub.docker.com/v2/repositories/bitnamilegacy/redis/tags/8.2.1-debian-12-r0
200

To get a release running again while you migrate, point the chart at the legacy repository. Recent Bitnami charts verify image sources, so substituting a repository also needs global.security.allowInsecureImages, a value present in the current chart:

# Temporary bridge only: bitnamilegacy images receive no security fixes.
helm upgrade --install cache oci://registry-1.docker.io/bitnamicharts/redis \
  --namespace cache \
  --set image.repository=bitnamilegacy/redis \
  --set global.security.allowInsecureImages=true

Charts with sidecars (metrics exporters, os-shell init containers) need the same override for each image key; helm get values and kubectl get pod -o jsonpath='{..image}' show what is still pointing at bitnami/. When I scanned bitnamilegacy/redis:7.4 this week it had 214 unique CVEs, 112 with published fixes it will never get, so put a date on the migration. The longer comparison of replacements is in Hardened Container Images in 2026.

Cause 3: Private registry credentials and imagePullSecrets

A 401 or 403 on a registry that is not your cloud provider's means the kubelet sent no credentials or the wrong ones. Kubernetes uses a Secret of type kubernetes.io/dockerconfigjson, and the Secret must live in the same namespace as the pod.

# Create the secret from a read-only token, not your password.
# Reading the token from a file keeps it out of shell history.
kubectl create secret docker-registry ghcr-pull \
  --namespace my-app \
  --docker-server=ghcr.io \
  --docker-username=my-bot \
  --docker-password="$(cat ~/.secrets/ghcr-read-token)"

# Attach it to the service account the pods use, so every pod picks it up
kubectl patch serviceaccount default -n my-app \
  -p '{"imagePullSecrets": [{"name": "ghcr-pull"}]}'

Or reference it directly in the pod spec:

spec:
  imagePullSecrets:
    - name: ghcr-pull
  containers:
    - name: api
      image: ghcr.io/myorg/api:1.8.3

Things to check when it still fails:

# Server key must match the image host exactly (ghcr.io vs https://ghcr.io/v2/ mismatches bite)
kubectl get secret ghcr-pull -n my-app -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq '.auths | keys'

# Is the secret attached to the service account this pod actually uses?
kubectl get pod my-app-7c9f8d6b5-x2lqk -o jsonpath='{.spec.serviceAccountName}{"\n"}{.spec.imagePullSecrets}{"\n"}'

For Docker Hub, the server value in the Kubernetes docs is https://index.docker.io/v1/. For Docker Hardened Images, Docker's Kubernetes guide says you must authenticate to dhi.io even for the free community images, using a personal or organisation access token with read access, and create the secret in every namespace that uses them. If you need short-lived credentials instead of static tokens, see Workload Identity Federation explained.

Cause 4: ECR and Artifact Registry permissions on the node

Here is the part that confuses people who have set up IRSA, EKS Pod Identity or GKE Workload Identity: those give pods an identity for API calls, but the image pull is done by the kubelet, using the node's identity. A pod with perfect IRSA can still get 403 on its own image.

Amazon EKS and ECR. The node IAM role needs AmazonEC2ContainerRegistryPullOnly (or an equivalent custom policy with ecr:GetAuthorizationToken, ecr:BatchGetImage, ecr:GetDownloadUrlForLayer and ecr:BatchCheckLayerAvailability, per the ECR on EKS docs). Fargate uses the pod execution role instead.

# Find the node role for a managed node group
aws eks describe-nodegroup --cluster-name prod --nodegroup-name general \
  --query 'nodegroup.nodeRole' --output text

# List what is attached to it
aws iam list-attached-role-policies --role-name prod-general-node-role

# Attach the pull policy if it is missing
aws iam attach-role-policy --role-name prod-general-node-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPullOnly

If the repository is in another AWS account, the node role policy is not enough: the repository policy in the other account must also allow the node role's principal to pull.

GKE and Artifact Registry. The node service account needs roles/artifactregistry.reader on the repository, and the node needs a storage read or cloud-platform access scope. Google's image pull troubleshooting page maps failed to fetch oauth token: unexpected status: 403 Forbidden to missing permission and the 401 variant to a disabled service account or missing scope.

# Which service account do the node pools use? (Standard clusters)
gcloud container clusters describe prod --location=europe-west2 \
  --format="table(nodePools.name,nodePools.config.serviceAccount)"

# Grant read on the repository (works across projects too)
gcloud artifacts repositories add-iam-policy-binding app-images \
  --location=europe-west2 \
  --member=serviceAccount:gke-nodes@my-project.iam.gserviceaccount.com \
  --role="roles/artifactregistry.reader"

Access scopes cannot be changed on existing nodes; Google's fix is a new node pool with --scopes="gke-default".

Kubernetes is closing the identity gap: since 1.34 the kubelet can pass the pod's service account token to a credential provider plugin, a beta feature enabled by default. It only helps if your credential provider is configured with tokenAttributes, so for managed clusters today, assume node identity.

Cause 5: Docker Hub rate limits (429 Too Many Requests)

Docker's published limits are 100 pulls per 6 hours per IPv4 address or IPv6 /64 for anonymous pulls, 200 per 6 hours for free Personal accounts, and unlimited (subject to fair use) for Pro, Team and Business. A multi-arch pull counts once per architecture. Every node behind one NAT gateway shares a single anonymous allowance, which is why this hits during a node pool roll-out and not on your laptop.

You can check what the registry is applying to your IP. HEAD requests do not count against the limit:

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

When I ran this anonymously on 20 October 2026 the response was ratelimit-limit: 100;w=3600 and ratelimit-remaining: 100;w=3600: a window of 3,600 seconds, not the 21,600 the docs page describes. So measure from your own egress IP rather than trusting either number. Fixes, in the order I would apply them:

  1. Authenticate pulls with an imagePullSecret from a paid Docker account (Cause 3). Even a free account doubles the anonymous allowance.
  2. Mirror through your cloud registry. ECR pull through cache supports Docker Hub with credentials stored in a Secrets Manager secret whose name starts with ecr-pullthroughcache/; Artifact Registry has remote repositories for the same job.
  3. Copy the images you depend on into your own registry in CI, so production never pulls from Docker Hub at all.

Cause 6: Architecture mismatch (arm64 vs amd64)

Graviton, Ampere and Apple-silicon build machines made this common. If the image index has no entry for the node's platform, the pull fails. This is the real error from Docker 28 for an image that does not publish linux/arm/v7:

$ docker pull --platform linux/arm/v7 gcr.io/distroless/nodejs24-debian13:latest
Error response from daemon: no matching manifest for linux/arm/v7 in the manifest list entries: no match for platform in manifest: not found

containerd on a node reports the no match for platform in manifest part. A related trap: if someone pushed a single-architecture image (no index), the pull succeeds on the wrong node and the container dies with exec format error, which shows up as CrashLoopBackOff, not ImagePullBackOff.

# What platforms does the image publish?
docker buildx imagetools inspect ghcr.io/myorg/api:1.8.3 | grep Platform

# What are the nodes?
kubectl get nodes -L kubernetes.io/arch

# Build and push both architectures from one command (Docker Buildx)
docker buildx build --platform linux/amd64,linux/arm64 \
  -t ghcr.io/myorg/api:1.8.4 --push .

See Docker's multi-platform build docs. If you cannot rebuild yet, keep the workload on matching nodes with nodeSelector: {kubernetes.io/arch: amd64}.

Cause 7: Network, DNS, proxies and very large images

dial tcp: lookup ... server misbehaving is DNS on the node, not in the pod. i/o timeout usually means private nodes with no route to the registry: missing NAT, missing VPC endpoints for ECR (ecr.api, ecr.dkr and the S3 gateway endpoint), or a corporate proxy that containerd is not configured to use. context canceled on a multi-gigabyte image is a pull that took too long; GKE's troubleshooting page lists it under timeouts for very large images. Test from the node itself, since kubectl exec into a pod uses the pod's network path and cluster DNS:

kubectl debug node/ip-10-0-12-34.eu-west-2.compute.internal -it --image=busybox:1.37 -- \
  sh -c 'nslookup registry-1.docker.io; wget -S -O /dev/null https://registry-1.docker.io/v2/ 2>&1 | head -3'

A 401 from that wget is good news: it means DNS and TLS work and the registry answered. For large images, shrinking them pays twice; see reducing Docker image size with multi-stage builds.

Troubleshooting

Symptom Likely cause Fix
not found / manifest unknown Typo, missing tag, implicit docker.io or :latest Use the full reference from CI; pin by digest
not found on docker.io/bitnami/<app>:<version> Tag moved to bitnamilegacy in Aug 2025 Override repository temporarily; migrate off
Helm refuses the override with an image verification error Bitnami chart image check Add global.security.allowInsecureImages=true
401 Unauthorized on a private registry No secret, wrong namespace, wrong server key Create secret in pod namespace; attach to service account
403 Forbidden from ECR Node role lacks ECR pull, or cross-account repo policy Attach AmazonEC2ContainerRegistryPullOnly; update repo policy
failed to fetch oauth token ... 403 on GKE Node service account lacks artifactregistry.reader Grant role on the repository
429 Too Many Requests from Docker Hub Shared anonymous limit behind NAT Authenticate; use ECR or Artifact Registry cache
no match for platform in manifest Image lacks the node's architecture buildx --platform linux/amd64,linux/arm64; nodeSelector
Pull succeeds, then exec format error Single-arch image on wrong node Same as above; it is a CrashLoopBackOff
i/o timeout / context canceled No egress, missing VPC endpoints, huge image Fix routes or endpoints; shrink the image

FAQ

What is the difference between ErrImagePull and ImagePullBackOff?

ErrImagePull is the status right after a pull attempt fails. ImagePullBackOff is the state between retries, while the kubelet waits with an increasing delay capped at 300 seconds. The cause is identical; read the event message to find it.

How do I force Kubernetes to retry the image pull immediately?

Fix the cause, then delete the pod (kubectl delete pod <name>) so its controller creates a new one, or run kubectl rollout restart deployment/<name>. Waiting also works, but at the 300-second cap that can take five minutes per attempt.

Why do my pods get 403 from ECR when IRSA is configured?

IRSA and EKS Pod Identity give the running container AWS credentials. The image is pulled earlier by the kubelet with the node IAM role, so the node role needs ECR pull permissions. Kubernetes 1.34 added a beta path for pod-identity-based pulls through credential provider plugins, but it needs explicit configuration.

Do Docker Hub rate limits apply to Kubernetes clusters?

Yes. Every node pulling anonymously from behind the same NAT IP shares one anonymous allowance, so a large roll-out can exhaust it in minutes. Authenticated pulls or a registry mirror remove the problem.

When this happens on a cluster I run, the first thing I check is the event message, and the second is whether the image reference has a registry, a tag and a digest; those two catch most of it. In production I never let nodes pull from Docker Hub directly: images are copied or cached into the cloud registry in CI, pinned by digest, and scanned there, which turns rate limits, Bitnami-style catalogue moves and upstream deletions into a CI failure instead of a 3 a.m. page. The official references are the Kubernetes images documentation, pulling from a private registry, Docker Hub pull limits, ECR on EKS and GKE image pull troubleshooting.

Related: Kubernetes as the Agent Control Plane in 2026, The Worms Learned to Use Your AI Agent and CRA Reporting Went Live on 11 September.

I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.

Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.


Related