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, not latest), and avoid --pull always or docker compose pull on 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.

  • Docker
  • Registry
  • Troubleshooting