Docker Hub limits how many image pulls an anonymous client can make, counted per IP address over a rolling six hours. Authenticated free accounts get a higher allowance, and paid accounts higher still.
The per-IP part is why this bites CI and not your laptop. A shared NAT gateway, a Kubernetes node pool or a CI runner fleet all appear as one or a handful of addresses, so the whole team's pulls count together, and a cluster scaling up pulls the same base image once per node.
It is also why the error arrives suddenly with no change on your side: someone else exhausted the shared budget.
Confirm it is the rate limit
docker pull nginx:latest
Error response from daemon: toomanyrequests: You have reached your pull rate limit.
In Kubernetes it surfaces as ImagePullBackOff with a 429 in the events:
kubectl describe pod api-7d4f-x8k2 | grep -A3 Failed
Warning Failed kubelet Failed to pull image "nginx:latest": ... 429 Too Many Requests
Check what your current allowance looks like:
TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/nginx:pull" | python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')
curl -sI -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/library/nginx/manifests/latest | grep -i ratelimit
ratelimit-limit: 100;w=21600
ratelimit-remaining: 3;w=21600
w=21600 is the six-hour window in seconds. Three pulls left.
Fix 1: Authenticate (five minutes)
The cheapest improvement, and it changes the counter from your shared IP to your account.
docker login -u myuser
In GitHub Actions:
- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
In GitLab CI:
before_script:
- echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USER" --password-stdin
Use an access token, not your password. It can be scoped to read-only and revoked independently.
For Kubernetes, create a pull secret and attach it to the default service account so every pod in the namespace benefits without editing each manifest:
kubectl create secret docker-registry dockerhub \
--docker-server=https://index.docker.io/v1/ \
--docker-username=myuser \
--docker-password="$DOCKERHUB_TOKEN"
kubectl patch serviceaccount default \
-p '{"imagePullSecrets":[{"name":"dockerhub"}]}'
That patch is per namespace, so repeat it wherever you deploy, and note that pods already running are unaffected until they next pull.
Fix 2: Pull from somewhere else (ten minutes)
Many common images are mirrored on registries with no such limit:
# Docker Hub
FROM nginx:1.27-alpine
# Amazon ECR Public, no rate limit for AWS-authenticated pulls
FROM public.ecr.aws/nginx/nginx:1.27-alpine
# Google mirror of Docker Hub
FROM mirror.gcr.io/library/nginx:1.27-alpine
mirror.gcr.io is a drop-in read-through mirror of Docker Hub's library images, which makes it the lowest-effort change of all. Prefix the image name and nothing else moves.
Official images for most languages and databases are also published to public.ecr.aws, GitHub Container Registry (ghcr.io) and Quay.
Fix 3: ECR pull through cache (best fix on AWS)
If you are on AWS, this is the one worth doing properly. ECR caches upstream images on first pull and serves every subsequent pull from your own registry:
aws ecr create-pull-through-cache-rule \
--ecr-repository-prefix dockerhub \
--upstream-registry-url registry-1.docker.io \
--credential-arn "$SECRET_ARN"
FROM 123456789012.dkr.ecr.eu-west-1.amazonaws.com/dockerhub/library/nginx:1.27-alpine
Three benefits beyond the rate limit: pulls stay inside AWS so they are faster and incur no internet data transfer, images survive upstream deletion, and ECR image scanning applies to them.
The --credential-arn points at a Secrets Manager secret holding Docker Hub credentials, which raises the upstream allowance for the cache's own fetches.
Fix 4: A registry mirror for the cluster
Configure the container runtime on every node to route Docker Hub pulls through a mirror, so no manifest changes at all:
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
endpoint = ["https://mirror.gcr.io", "https://registry-1.docker.io"]
sudo systemctl restart containerd
Listing the upstream second means a cache miss still works. Apply it through your node bootstrap or a DaemonSet so new nodes inherit it. A mirror configured by hand on existing nodes disappears the next time the group scales.
For a self-hosted option, a pull-through registry:2 or Harbor instance does the same and keeps everything inside your network.
Fix 5: Pull less
Worth doing regardless of the above.
Pin tags. FROM node:latest re-pulls whenever the tag moves; FROM node:22.11.0-alpine is cached far more often.
Cache images in CI. A build that pulls the same base image on every run is wasting both allowance and time. Use your CI's Docker layer cache, or pull once into a self-hosted runner.
Set imagePullPolicy deliberately. Always on a pinned tag pulls on every pod start for no benefit:
imagePullPolicy: IfNotPresent
Note that Kubernetes defaults to Always when the tag is latest, which is one more reason not to use it.
Use multi-stage builds so you are not pulling a large base for the final image.
A checklist
- Confirm with the
ratelimit-remainingheader, not guesswork. docker loginwith an access token. The fastest improvement.- Kubernetes → pull secret patched onto the namespace's default service account.
- Quick win → prefix images with
mirror.gcr.io/library/or usepublic.ecr.aws. - On AWS → ECR pull through cache, which fixes it permanently.
- Cluster-wide → containerd registry mirror applied via node bootstrap.
- Pin tags, set
imagePullPolicy: IfNotPresent, cache in CI. - Remember the limit is per IP, so a shared NAT means shared blame.
Frequently Asked Questions
Why am I hitting Docker Hub's rate limit when I have barely pulled anything?
Because anonymous pulls are counted per IP address, not per user. A CI runner fleet, a Kubernetes node pool, or an office behind one NAT gateway all present as a small number of addresses, so everyone's pulls share the same budget, and a cluster scaling up pulls the same base image once per node. That is also why the error appears suddenly with nothing changed on your side: somebody else exhausted the allowance.
How do I check how many pulls I have left?
Request a token for any library image and read the response headers: ratelimit-limit and ratelimit-remaining, with w=21600 indicating the six-hour rolling window. It is worth doing before assuming the limit is the cause, since a 429 can also come from an upstream proxy. Authenticating changes which counter you are measured against, so run the check with and without credentials to confirm the login is taking effect.
How do I authenticate Docker Hub pulls in Kubernetes?
Create a docker-registry secret with your username and an access token, then patch it onto the namespace's default service account as an imagePullSecrets entry, that way every pod in the namespace uses it without editing individual manifests. The patch is per namespace, so repeat it wherever you deploy. Pods already running keep their current images until they next pull, so the change is not immediately visible.
What is the ECR pull through cache and why is it the best fix?
A rule that makes your own ECR registry a transparent cache in front of an upstream registry: the first pull of an image fetches it from Docker Hub, and every pull after that is served from ECR. You reference images by their ECR path, so the rate limit stops applying entirely. It also keeps pulls inside AWS, faster and no internet data transfer cost, protects you from upstream deletions, and brings the images into ECR's image scanning.
Can I avoid Docker Hub without changing every image reference?
Yes, two ways. Prefix images with mirror.gcr.io/library/, Google's read-through mirror of Docker Hub library images, which is a one-word change per reference. Or configure a registry mirror in containerd on every node, so docker.io pulls are transparently routed elsewhere and no manifest changes at all. List the upstream registry second in the mirror endpoints so a cache miss still succeeds, and apply the config through node bootstrap so new nodes inherit it.
Does imagePullPolicy affect the rate limit?
Significantly. Always makes the kubelet contact the registry on every pod start even when the image is already on the node, which on a busy cluster multiplies your pull count for no benefit. Set IfNotPresent with pinned tags so an image already present is reused. Note that Kubernetes silently defaults to Always when the tag is latest, which is one more reason to pin versions rather than relying on it.