Kubernetes: Pod stuck in ImagePullBackOff
Hi, it's BlueByte. A Pod that never leaves ImagePullBackOff isn't crashing — on paper it scheduled fine, but the kubelet can't fetch the container image, so it keeps retrying and backing off. The symptom is a Pod stuck at 0/1 with STATUS: ImagePullBackOff (or ErrImagePull a moment earlier). We'll walk through what those two states mean, how to read the Events to find the real reason, fix it per cause, and keep it from coming back.
What ImagePullBackOff and ErrImagePull are telling you
Two related states show up in kubectl get pods:
NAME READY STATUS RESTARTS AGE
web-6d4b8f9c7c-2xk9p 0/1 ImagePullBackOff 0 3mErrImagePull is the immediate failure: the kubelet asked the container runtime to pull the image and the pull failed. ImagePullBackOff is what follows — after repeated failures the kubelet waits before trying again, increasing the delay each time up to a five-minute cap. Neither means your application is broken. The container never started, because its image never arrived.
Why the kubelet can't pull the image
The pull fails for one of a few concrete reasons:
- The image name or registry is wrong — a typo, or a missing registry prefix so it defaults to Docker Hub.
- The tag doesn't exist — the image is real but
:1.2.3was never pushed, or you meant:1.2.3and typed:1.23. - The image is private and the Pod has no credentials — the registry answers with an authorization error.
- You hit a registry rate limit (Docker Hub throttles anonymous pulls), so the pull is refused for now.
imagePullPolicy: Neveris set but the image isn't already present on the node.
Read the Events to see the real reason
Don't guess — the Events at the bottom of kubectl describe name the exact failure:
kubectl describe pod web-6d4b8f9c7c-2xk9pEvents:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning Failed 30s (x4 over 2m) kubelet Failed to pull image "myrepo/app:1.2.3":
manifest unknown: manifest unknown
Warning Failed 30s (x4 over 2m) kubelet Error: ErrImagePull
Normal BackOff 5s (x6 over 2m) kubelet Back-off pulling image "myrepo/app:1.2.3"The Message is the whole story. manifest unknown means the tag doesn't exist; pull access denied or unauthorized means it's private and unauthenticated; no such host means the registry name is wrong. Confirm by pulling the same image by hand from a node or your laptop:
docker pull myrepo/app:1.2.3If that fails the same way, the problem is the image or the credentials, not Kubernetes.
Fix it by cause: name, tag, private registry
- Wrong name or tag — verify the tag exists in the registry, then correct the reference and re-apply:
kubectl set image deployment/web app=myrepo/app:1.4.0- Private registry — create a pull secret and attach it to the Pod template:
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=<user> \
--docker-password=<token>spec:
imagePullSecrets:
- name: regcred- Rate limited on Docker Hub — authenticate with the same secret mechanism, or mirror the image into your own registry.
A real case: a private image with no pull secret
You deploy registry.example.com/team/api:2.1.0 and the Pod sits at ImagePullBackOff. kubectl describe pod shows Failed to pull image ...: pull access denied, repository does not exist or may require authorization. The image is real and pushed, so it's authorization. You create the pull secret with kubectl create secret docker-registry regcred ..., add imagePullSecrets: [{ name: regcred }] to the Deployment's Pod template, and run kubectl rollout restart deployment/api. Within seconds the new Pod pulls the image and reaches 1/1 Running. The registry was reachable all along — the kubelet just had no credentials to present.
Confirm the Pod is actually Running
After the fix, watch the Pod come up:
kubectl get pod -l app=api -wNAME READY STATUS RESTARTS AGE
api-7c9d5f8b6d-abcde 1/1 Running 0 12s1/1 and Running with no restarts means the image pulled and the container started. kubectl describe pod now shows a Pulled event instead of Failed.
Keep it from recurring
Pin images to a real, immutable tag or a digest rather than :latest, so a Pod can't land on a tag that was retagged or removed — with :latest the default imagePullPolicy is even Always, so every restart re-pulls. Store registry credentials as a Secret referenced by every workload that needs the private registry, and for Docker Hub at scale, pull through an authenticated mirror to stay under the rate limit. Validate the spec with kubectl apply --dry-run=server -f pod.yaml before rollout to catch a bad image reference early.
How this differs from CrashLoopBackOff
ImagePullBackOff happens before the container runs — the image never arrived. CrashLoopBackOff happens after: the image pulled fine, the container started, then exited repeatedly. If kubectl describe shows a Pulled event but the Pod still restarts, that's CrashLoopBackOff and you should read the container logs, not the image reference. Both use the same exponential back-off, but the cause sits on opposite sides of "did the image load."
Related questions
The Pod says ErrImagePull, not ImagePullBackOff. Is that a different problem?
Same problem, earlier stage. ErrImagePull is the immediate pull failure; ImagePullBackOff is the kubelet waiting between retries after it keeps failing. Read the same Events in kubectl describe either way.
kubectl describe shows 'pull access denied'.
The image is private and the Pod has no credentials. Create a docker-registry Secret with kubectl create secret docker-registry and reference it under imagePullSecrets in the Pod template.
It says 'manifest unknown' but the image exists.
The repository exists; that specific tag does not. List the tags in the registry and pin to one that was actually pushed, and check for typos like :1.23 versus :1.2.3.
It pulls fine on my laptop but not in the cluster.
Your laptop is logged in to the registry; the nodes are not. Give the Pod a pull secret via imagePullSecrets, or ensure the node's container runtime has credentials for that registry.
How long does the back-off last?
The kubelet increases the delay on each retry up to a five-minute cap and keeps retrying — it does not give up. Fixing the image reference or the secret is enough; the next scheduled retry pulls successfully.
References
Haneul Seo
Infrastructure engineer · 10+ years running Linux fleets
More in this category
Git: fatal: detected dubious ownership in repository
Since the CVE-2022-24765 fix in Git 2.35.2, Git refuses to read a repository whose working tree or .git directory is owned by a different user than the one running the command. It shows up in containers, CI jobs, sudo sessions and shared drives. Fix the ownership if the repo should be yours, or add the exact path to safe.directory in your global config — never in the repo's own config, which Git ignores for this.
Kubernetes: Internal error occurred: failed calling webhook
An admission webhook sits in front of your write, the API server could not get an answer out of it, and failurePolicy: Fail turned that silence into a rejection. The tail of the message is the whole diagnosis: context deadline exceeded means the call went nowhere, no endpoints available means nothing is running, and an x509 line means the API server does not trust the webhook's certificate. Each has a different fix, and none of them is your manifest.
MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
A statement waited the full innodb_lock_wait_timeout for a row lock another transaction is still holding, and gave up. sys.innodb_lock_waits names the blocking session and hands you the KILL statement, and a blocking_query of NULL means the blocker is idle on an open transaction. The detail most retry loops get wrong: by default only the timed-out statement is rolled back, so your transaction is still open and still holds every lock it took earlier.
Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk
Reads keep working and every write is rejected, because the last background save failed and stop-writes-on-bgsave-error defaults to yes. The log names the real cause — no space, a dir the redis user can't write, a read-only mount at rename time, or fork failing with Cannot allocate memory. Fix the cause, run one BGSAVE, and writes come back on their own with no restart: rdb_last_bgsave_status flips from err to ok. Setting stop-writes-on-bgsave-error no restores writes instantly but leaves the snapshot broken, so treat it as a deliberate trade, not the fix.
Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (exit 134)
The V8 heap has its own ceiling, derived from system memory and the Node release, and it is often far below the RAM you have; when a build or server reaches it, V8 aborts with FATAL ERROR: Reached heap limit and exit code 134. Read the real limit with v8.getHeapStatistics().heap_size_limit, then raise it with --max-old-space-size (in MiB) or NODE_OPTIONS for a large workload, size it below the cgroup limit inside containers, and use --heapsnapshot-near-heap-limit to catch a leak in a long-running process. Exit 137 with no FATAL ERROR line is a container kill, not this.
Docker: "exec format error" when the container starts — wrong-platform image, no emulator, or a script with no shebang
The container exits on its first instruction with exec format error — the kernel's ENOEXEC, meaning the file exists but cannot be executed here. In practice that is an image built on one CPU architecture (an Apple-silicon Mac produces linux/arm64) and run on another (an x86_64 server) with no QEMU handler registered in binfmt_misc, or an entrypoint script whose first line is not a shebang. uname -m, docker image inspect and ls /proc/sys/fs/binfmt_misc tell the causes apart; the fix is an explicit docker buildx build --platform (or a manifest list for both), QEMU registration or --platform when you mean to emulate, and a #!/bin/sh line for the script.