If a deploy or CI job fails with:
toomanyrequests: You have reached your pull rate limit.
…you’ve hit Docker Hub’s pull rate limit. Anonymous pulls are limited per IP address, and authenticated free accounts get a higher limit, with paid plans higher still. Servers that pull often (CI runners, auto-updaters, many containers) hit it easily. Here’s how to check your status, and stop it from happening.
Exact limits change over time: check Docker’s current documentation for the numbers that apply to your account type.
Check your remaining pulls
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
ratelimit-limit: 100;w=21600
ratelimit-remaining: 76;w=21600
w=21600 is the window in seconds. Use credentials (-u user:token on the token request) to see your authenticated limit.
Fix 1: log in on servers and CI
Authenticated pulls get a higher limit, counted per account rather than per IP:
docker login -u youruser # use an access token, not your password
Create a personal access token (read-only) in Docker Hub’s account settings. On CI, store it as a secret, and run docker login before builds. For Kubernetes, use an imagePullSecret. See Docker secrets.
Fix 2: pull less often
- Pin image tags (
nginx:1.27.2, notlatest), and avoid--pull alwaysordocker compose pullon every deploy when nothing changed. See Docker image tags best practices. - Auto-updaters (Watchtower) checking many images every few minutes consume pulls quickly. Lengthen the interval. See Watchtower.
- CI caches: keep base images on long-lived runners instead of pulling on every job.
Note: checking whether an image changed (a HEAD request on the manifest) doesn’t count as a pull, but downloading it does.
Fix 3: run a pull-through cache (registry mirror)
A local registry in proxy mode caches Docker Hub images: the first pull fetches from Docker Hub, and later pulls (from any of your servers) come from the cache.
# /opt/registry-mirror/compose.yaml
services:
mirror:
image: registry:2
restart: unless-stopped
environment:
REGISTRY_PROXY_REMOTEURL: https://registry-1.docker.io
REGISTRY_PROXY_USERNAME: youruser # authenticated upstream pulls
REGISTRY_PROXY_PASSWORD: your-access-token
volumes:
- ./data:/var/lib/registry
ports:
- "10.8.0.1:5000:5000" # private network/VPN only
On each Docker host, in /etc/docker/daemon.json:
{ "registry-mirrors": ["http://10.8.0.1:5000"] }
sudo systemctl restart docker
docker info | grep -A1 "Registry Mirrors"
Keep the mirror on a private network or VPN (or behind TLS and authentication). An open mirror can be abused. A storage VPS in the same location makes a good cache host. See Docker private registry, Harbor, and storage VPS.
Fix 4: use other registries
Many images are also published elsewhere, often without strict limits:
| Registry | Examples |
|---|---|
GitHub Container Registry (ghcr.io) |
Many open-source projects |
| Quay.io | Red Hat and community images |
Amazon ECR Public (public.ecr.aws) |
Official images mirror (public.ecr.aws/docker/library/nginx) |
Google (mirror.gcr.io) |
Cache of popular Docker Hub images |
| Your own registry | Push the images you depend on |
Quick reference
| Fix | How |
|---|---|
| Authenticate | docker login with an access token |
| Fewer pulls | Pin tags, slower auto-update checks, CI caching |
| Cache | registry:2 in proxy mode + registry-mirrors |
| Alternative source | ghcr.io, quay.io, public.ecr.aws |
| Check limits | ratelimit-remaining header |
Related: Docker commands cheat sheet and Docker Compose in production.