Docker

toomanyrequests: You have reached your pull rate limit

Docker Hub is throttling anonymous pulls by IP, which is why CI breaks and a laptop does not. The five fixes, in order of how much work they are.

medium fix6 min read

docker. The error
Error response from daemon: toomanyrequests: You have reached your pull rate
limit. You may increase the limit by authenticating and upgrading:
https://www.docker.com/increase-rate-limit

Failed to pull image "nginx:latest": rpc error: code = Unknown desc = failed
to pull and unpack image "docker.io/library/nginx:latest": failed to copy:
httpReadSeeker: failed open: unexpected status code 429 Too Many Requests

Do this first3 steps

Run these in order. Each one tells you what its output means before you change anything.

  1. 1

    Confirm it is the limit and see what is left

    docker run --rm curlimages/curl -s https://auth.docker.io/token?service=registry.docker.io\&scope=repository:library/nginx:pull | head -c 80

    If even this fails you are already at zero. Anonymous pulls are counted per source IP, so an entire NAT'd cluster or CI fleet shares one budget and exhausts it far faster than any one person would.

  2. 2

    Authenticate, which gets you your own budget

    docker login -u myuser

    An authenticated account is counted per user instead of per IP, which alone resolves most cases. On Kubernetes create a docker-registry secret and attach it to the service account rather than to each Pod.

  3. 3

    Stop depending on Docker Hub for the durable fix

    kubectl patch serviceaccount default -p '{"imagePullSecrets":[{"name":"dockerhub"}]}'

    Then move base images behind a pull-through cache. ECR, Artifactory or a registry mirror. A cluster that pulls the same base image on every node is the thing actually burning the quota.

All 8 sections

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

  1. Confirm with the ratelimit-remaining header, not guesswork.
  2. docker login with an access token. The fastest improvement.
  3. Kubernetes → pull secret patched onto the namespace's default service account.
  4. Quick win → prefix images with mirror.gcr.io/library/ or use public.ecr.aws.
  5. On AWS → ECR pull through cache, which fixes it permanently.
  6. Cluster-wide → containerd registry mirror applied via node bootstrap.
  7. Pin tags, set imagePullPolicy: IfNotPresent, cache in CI.
  8. 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.

Learn the underlying concept

Other Docker errors