skip to content

A `docker pull` from Docker Hub fails with `toomanyrequests: You have reached your pull rate limit`. What is Docker Hub actually counting, how does the limit differ for anonymous versus authenticated pulls, and what is your first fix?

level: juniorimportance: must knowfreq 60%

answer

  1. 429 = toomanyrequests
  2. counts manifests, not bytes
  3. anonymous = per source IP
  4. authenticated = per Docker ID
  5. ratelimit-remaining header via ratelimitpreview/test

basics

~20 s

Docker Hub counts image manifest requests, not layers or bytes. Anonymous pulls are counted per source IP address and get the smallest quota; authenticated pulls count against your Docker account and get more; paid plans get more still. First fix: docker login on the machine that pulls.

solid answer

~50 s

Docker Hub meters **pulls**, and a pull means a **manifest request**, not layer bytes and not `docker run`. Even an image already in the local cache can consume quota, because the client still fetches the manifest to check the tag still resolves to the same digest. Quota is attributed to the identity of the request. Anonymous requests are bucketed **by source IPv4 address**, so an office or a CI fleet behind one NAT gateway shares a single bucket. Authenticated requests are attributed to the **Docker account**, with a higher allowance; paid tiers get much higher or effectively unlimited. The exact numbers have been revised repeatedly, so give the mechanism and the tier ladder rather than betting on a figure. Over the limit the registry returns HTTP 429 and the CLI prints `toomanyrequests`. First fix: `docker login` — in CI with a dedicated machine account and a read-only access token, never a personal password. Then a pull-through mirror, internal copies of base images, or a paid plan.

code

bash · 5 lines
bash
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

go deeper

for a junior

Recognise the error, know it is Docker Hub throttling, and know that docker login is the first thing to try.

for a middle

Explain that manifest requests are the metered unit and that anonymous quota is per source IP while authenticated quota is per account; know where to check remaining quota.

for a senior

Connect it to fleet topology — shared NAT egress, cold node caches, ImagePullBackOff in clusters — and pick between authentication, mirroring and internal base images with cost in mind.

for a principal

Frame Docker Hub as an availability and supply-chain dependency: decide organisation-wide whether builds may reach upstream at all, and what the contractual/ownership story is for base images.

## What is being counted Docker Hub's limit is not bandwidth. It counts **manifest requests**: the `GET`/`HEAD` on `/v2/<repository>/manifests/<tag-or-digest>` that a client issues to discover what an image is made of. Layer (blob) downloads are not counted separately, so a 2 GB image and a 5 MB image cost the same. The counterintuitive consequence: a machine that already has the image cached still spends quota, because pulling a *tag* requires re-resolving it to a digest. Anything that shells out to `docker pull`, a `FROM` line in a build, a Kubernetes node whose pull policy re-checks a tag — each is a chargeable event. ## How the tiers work The registry attributes each request to an identity: - **Anonymous**: bucketed by **source IPv4 address**. Everyone behind one NAT gateway, VPN egress, or cloud NAT shares one counter. This is why the error appears on shared networks and CI runners even though 'we barely pull anything'. - **Authenticated free account**: bucketed by **Docker ID**, and travels with the credential rather than the network. Notably higher than anonymous. - **Paid plans (Pro / Team / Business)**: substantially higher or effectively unlimited under fair use. Docker has repeatedly re-cut the actual numbers — the original scheme was per-6-hour windows (roughly 100 anonymous / 200 authenticated), later revisions moved to much tighter hourly per-IP anonymous limits with authenticated and paid tiers above. Interviewers care that you know it is *per-IP when anonymous, per-account when logged in, higher when paid*; quoting a stale number confidently is worse than saying the numbers move. ## Recognising it The registry answers **HTTP 429 Too Many Requests**; the Docker CLI surfaces it as `toomanyrequests: You have reached your pull rate limit.` In Kubernetes it appears as `ImagePullBackOff` with the same message in the pod events — which is why a cluster that has been healthy for weeks can suddenly fail to schedule after a node reboot flushes the image cache. ## Checking remaining quota Docker exposes a preview endpoint: fetch a token for the throwaway repository `ratelimitpreview/test` and issue a `HEAD` on its manifest — the response carries `ratelimit-limit` and `ratelimit-remaining` headers with a window in seconds. Unlimited accounts simply omit the headers. Treat this as a diagnostic aid, not an API to build on. ## Fixes, in the order you should reach for them 1. **Authenticate.** Cheapest, immediate. In CI use a machine account plus a scoped, read-only **access token** stored as a secret, and `docker login` in a setup step. This also stops one noisy neighbour on the NAT from breaking everyone else. 2. **Cache.** Point the daemon at a **pull-through mirror** so N agents cost roughly one upstream pull per image, or preload the image into the runner AMI/node image. 3. **Own your base images.** Copy the handful of bases you actually depend on into an internal registry and rewrite `FROM` lines to it. This also removes an availability dependency on Hub. 4. **Pay.** A Team seat is far cheaper than an engineering week of flaky builds. ## Anti-patterns Retrying harder does not help — the window is time-based, so a retry storm just keeps you pinned at zero. Rotating NAT addresses to dodge per-IP counting is quota evasion and is against Hub's terms. And `docker pull --disable-content-trust` or `--quiet` change nothing here; the cost is the manifest request itself.

  • Our runners already have the image cached locally, so why are they still burning quota?
    Because pulling by tag is a resolution step: the client asks the registry which digest `nginx:1.27` currently points to, and that manifest request is the metered event even when every layer is already on disk. Pinning by digest, or setting an image pull policy that does not re-check an existing image, avoids the round trip. A local pull-through cache also absorbs it.
  • Does logging in on the developer's laptop help the Kubernetes cluster?
    No — the quota follows whichever credential the *pulling* client presents. Cluster nodes pull with their own configuration, so you need an image pull secret (or node-level registry credentials) wired into the service account or pod spec, or a mirror configured on the container runtime. Fixing one machine never fixes the fleet.

It is a turnstile, not a weighbridge: you are charged for walking through the gate, not for how heavy your bag is — and if you walk in without a badge, the turnstile counts your whole building as one person.

saying these in an interview costs you the question

  • Saying the limit is about download bandwidth or image size.
  • Claiming a locally cached image never counts against the limit.
  • Quoting a specific number ("100 pulls per 6 hours") as current fact without noting Docker has revised it.
  • Proposing to cycle NAT IPs or spread pulls across egress addresses to evade per-IP counting.
  • Adding aggressive retries as the fix, which keeps the client pinned at the limit.

context