Skip to content
Blog

Docker Hub Rate Limits: How They Work and How to Cope

Docker Hub caps pulls per six-hour window for unauthenticated and Personal users. Learn exactly what counts as a pull, what the 429 looks like, and the mitigation ladder from CI auth to a pull-through cache.

Published on • October 17, 2026

AI Assistant

A CI job that worked fine all week starts failing at 03:00 with You have reached your pull rate limit, and the pipeline is red until the window resets. The pull budget was never visible, nobody logged in on the runner, and a matrix build quietly pulled the same base image forty times. Docker Hub rate limits are not a punishment - they are a quota with published rules, and once you know how pulls are counted you can usually stay under them without paying for anything.

This post covers the mechanics as documented by Docker: what counts as a pull, the exact per-plan numbers, what the two different 429 responses look like, and how to check your remaining budget from the command line. Then it walks the mitigation ladder in order of effort - authenticate, cache, mirror, pin, self-host - plus how to monitor who is burning your quota.

What actually counts as a pull

The Docker Hub pull usage and limits page defines a pull fairly precisely:

  • A pull includes both a version check and any download that results from it. Depending on the client, docker pull can verify that an image or tag exists without downloading it, by performing a version check.
  • Version checks do not count towards usage pricing.
  • A pull for a normal image counts as one pull for a single manifest.
  • A pull for a multi-architecture image counts as one pull for each architecture pulled.

That last rule is the one that surprises people most. docker pull --platform linux/amd64 postgres:18 on a single-arch request is one pull. Pulling a multi-arch index and letting the client fetch the manifest list plus the matching architecture manifest burns more than one. A build matrix across linux/amd64 and linux/arm64 against the same tag multiplies accordingly.

The practical consequence: the expensive thing is not docker pull as a command, it is repeated fresh resolution of tags from a cold client. Every CI runner that starts empty and pulls node:22, postgres:18, and a base image per job is spending pulls at a rate nobody watches until it stops.

The limits

Per Docker’s usage table, subject to fair use:

User typePull rate limit per 6 hours
Business (authenticated)Unlimited
Team (authenticated)Unlimited
Pro (authenticated)Unlimited
Personal (authenticated)200
Unauthenticated users100 per IPv4 address or IPv6 /64 subnet

Three details matter more than the numbers themselves:

The window is six hours, not an hour or a day. The headers Docker returns express it as w=21600 seconds.

Unauthenticated limits are keyed to network identity, not to an account. 100 pulls per IPv4 address or IPv6 /64 subnet means the whole office or the whole CI runner pool behind one NAT shares a single budget. Conversely, one authenticated user on a shared egress IP can be affected by other users’ traffic through the separate abuse limit (below).

Authentication moves the accounting from IP to identity. On the Personal plan, a logged-in account gets 200 per window. The attribution rules then decide where a pull is charged: private pulls go to the repository’s namespace owner; public pulls are attributed by verified domain ownership and organization membership - a single organization membership attributes to that organization, while multiple memberships under the same company attribute to the user’s personal namespace.

Paid plans (Pro, Team, Business) are unlimited but, as Docker notes, still subject to fair use.

What a 429 looks like

There are two distinct rate limits, and telling them apart is the first diagnostic step.

Pull rate limit. When you are over the limit, Docker Hub returns a 429 with this body on a manifest request:

You have reached your pull rate limit. You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limits

You will see it in the Docker CLI output or the Docker Engine logs.

Abuse rate limit. Docker Hub also has an abuse limit that protects the infrastructure and applies to all requests to Hub properties - web pages, APIs, and image pulls. It is applied per IPv4 address or IPv6 /64 subnet, applies to all users equally regardless of account level, and Docker describes its magnitude as “in the order of thousands of requests per minute,” varying with load. You distinguish it by the error: the abuse limit returns a plain 429 Too Many Requests, while the pull limit returns the longer message with the documentation link.

So: long message with a link = pull quota exhausted. Bare 429 Too Many Requests = something on your shared IP is hammering Hub, authenticated or not.

Checking your remaining budget

Docker documents a header-based way to read your quota. First get a token - anonymously, or with credentials if you are authenticated:

$ TOKEN=$(curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq -r .token)
$ TOKEN=$(curl --user 'username:password' "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq -r .token)

Then issue a HEAD request against the manifest:

$ curl --head -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest

The relevant headers:

ratelimit-limit: 100;w=21600
ratelimit-remaining: 20;w=21600
docker-ratelimit-source: 192.0.2.1

In this example the limit is 100 pulls per 21600 seconds (6 hours) with 20 remaining. Two cautions from the docs: a GET emulates a real pull and counts against the limit, a HEAD does not - which is exactly why this check is safe to script. And if you see no ratelimit header at all, the image or your IP may be unlimited through a publisher/provider/open-source partnership, or the account is on a paid subscription; pulls in that case do not count toward the rate limit.

#!/usr/bin/env bash
# Emit remaining pulls for the current identity/IP before starting a job.
TOKEN=$(curl -fsS "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq -r .token)
curl -fsSI -H "Authorization: Bearer $TOKEN" \
  https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest \
  | tr -d '\r' | grep -i '^ratelimit-'

Run that at the top of a pipeline and you turn a mysterious 03:00 failure into a visible gauge.

The mitigation ladder

Work up this list in order; each rung costs more than the last.

1. Authenticate everywhere

This is the cheapest fix and it is what Docker’s own 429 message tells you to do. On Docker Engine:

$ docker login --username "$DOCKERHUB_USER" --password-stdin <<< "$DOCKERHUB_TOKEN"

Use a personal access token rather than an account password for anything automated. Per Docker’s guidance by environment:

  • GitHub Actions: use docker/login-action (the action’s Docker Hub section covers username/token setup).
  • Docker Swarm services: pass --with-registry-auth to docker service create, otherwise the manager cannot forward credentials.
  • Kubernetes: follow the Pull an Image from a Private Registry instructions so kubelets authenticate per node.
  • Other CI systems: Docker lists provider-specific instructions for GitLab, CircleCI, AWS CodeBuild, Azure Pipelines, TeamCity, Drone, Codefresh and others on the pulls page.

One caveat worth internalizing: if a third-party platform pulls on your behalf, it may use one shared IPv4 address or IPv6 /64 subnet for many users, and even authenticated pulls from that address can trigger abuse rate limiting. Authentication fixes attribution; it does not fix a noisy neighbor.

2. Stop paying for the same manifest repeatedly

Authenticated Personal accounts still only get 200 per window, so the next lever is reducing demand:

  • Warm the cache once per job. Pull each base image once and let subsequent steps reuse the local image store instead of re-resolving tags.
  • Use BuildKit cache mounts and CI cache export/import so package-manager layers are not re-fetched on every run. The Dockerfile reference notes that RUN --mount=type=cache contents persist between builder invocations without invalidating the instruction cache - but that on CI runners which provision a fresh machine per job, cache mounts start empty every build unless you wire up a cache backend (Docker documents a GitHub Actions workaround for exactly this).
  • Collapse redundant matrix pulls. If every matrix leg pulls the same base, pull it once in a preliminary job and share the image, or make legs pull only what they need.
  • Be specific with tags. The Dockerfile best practices guide pushes --pull for fresh base images on a schedule rather than on every build, and .dockerignore to keep the build context lean.

3. Mirror or pull-through cache

For organizations that cannot reliably authenticate every egress - build fleets, air-gapped runners, multi-vendor CI - the standard pattern is a pull-through cache or mirror registry in front of Docker Hub: Harbor, Artifactory, Nexus, or a plain distribution registry configured with a proxy remote. Once the mirror is populated, internal pulls never touch Hub, and the Hub quota is consumed only on cache misses (plus periodic refreshes). Point every runner at the mirror by daemon configuration or by rewriting image references.

The trade-off is operational: someone owns freshness, GC policy, and storage for the mirror, and you inherit responsibility for vulnerabilities in whatever sits in cache. Mirror what you need, expire aggressively, and keep one authenticated path to upstream for refreshes.

4. Pin digests and reduce surprise churn

Not primarily a rate-limit tool, but it composes: FROM node:22@sha256:... makes the cache key stable, so rebuilds do not silently re-resolve tags. Docker’s best-practices page documents digest pinning as the way to guarantee the same image version across builds, with Dependabot package-ecosystem: "docker" or docker scout recommendations handling updates on a schedule you control instead of on every build.

5. Self-host, or pay

If pull volume is genuinely structural - you ship to thousands of nodes - the endpoints are a self-hosted registry (distribution/Harbor/Quay) that you populate once from Hub and then serve yourself, or a paid Docker subscription with an unlimited pull rate. Both convert an ongoing per-window quota into an upfront cost.

Monitoring and attribution

Docker exposes a Usage page in Docker Hub showing monthly pulls, and it can email a CSV of detailed pull records. The columns are designed for exactly this investigation:

ColumnWhat it tells you
datehourPeak usage times and patterns (yyyy/mm/dd/hh)
user_nameWhich Docker ID pulled - per-user accountability
access_token_nameWhich token was used; generated tokens are auto-created by the Docker client on sign-in
repository, tag, digestWhich images consume the budget
ipsEgress addresses involved (aggregated per hour)
version_checksVersion checks that did not count as pulls
pullsActual pulls per repository per hour

The access_token_name column is the fastest way to find the culprit: one shared token on forty runners versus a personal token on a developer laptop tells very different stories. Combine it with ips to catch the shared-IP abuse case, and repository to find the base image that should have been mirrored instead of pulled per job.

Checklist

  • Every automated pull path runs docker login (token, not password) - CI, Swarm with --with-registry-auth, Kubernetes pull secrets.
  • You know your plan’s number: 100 per IPv6 /64 or IPv4 unauthenticated, 200 per window on Personal, unlimited on Pro/Team/Business.
  • You can explain the counting rule: one manifest = one pull, multi-arch = one pull per architecture, version checks excluded.
  • You can tell the two 429s apart: long message with a link = pull limit; bare 429 Too Many Requests = abuse limit.
  • A HEAD check of ratelimit-remaining runs at the start of heavy jobs, never a GET.
  • Base images are pulled once per job and BuildKit/CI caches are wired up so package layers are not re-fetched.
  • High-volume or shared-IP fleets point at a pull-through mirror or a self-hosted registry.
  • The Hub Usage CSV is reviewed monthly to attribute pulls to users, tokens, and repositories.

Sources: Docker Hub pull usage and limits, Docker Hub usage and limits, Building best practices, Dockerfile reference - RUN —mount=type=cache, Kubernetes - Pull an Image from a Private Registry