In February 2025, Docker published new Docker Hub limits that would have capped unauthenticated clients at 10 pulls per hour and free Docker Personal accounts at 40 pulls per hour. The backlash was immediate — those numbers would have broken half the CI pipelines on the internet — and Docker walked them back within weeks. The limits that actually stuck, and that remain enforced today, are 100 pulls per 6 hours for unauthenticated clients (per IPv4 address) and 200 pulls per 6 hours for authenticated Personal accounts.
Here's the uncomfortable part: a busy team hits those numbers too. The 40-pull scare wasn't a false alarm — it was a preview of the direction of travel. This post does the arithmetic for a realistic CI pipeline and Kubernetes fleet, finds the point where a self-hosted registry cache stops being optional, and sizes the cache you'd run.
The Limits That Actually Apply
As of mid-2026, Docker Hub's pull limits look like this:
| Plan | Pull limit | Window | Scope |
|---|---|---|---|
| Unauthenticated | 100 pulls | Rolling 6 hours | Per IPv4 address (or IPv6 /64) |
| Docker Personal (authenticated) | 200 pulls | Rolling 6 hours | Per account |
| Pro / Team / Business | Unlimited | — | Fair-use policy applies |
Three counting rules matter for the math:
- A pull is a manifest fetch that leads to a download. Version checks (HEAD requests that don't download anything) are free.
- Multi-arch counts once per architecture actually pulled. Pull the same
node:22-alpinetag on an amd64 runner and an arm64 runner and you've spent 2 pulls, not 1. - The unauthenticated bucket is shared by everything behind your NAT. One office IP, one cluster egress IP — every laptop, runner, and node draws from the same 100.
When you exceed the bucket, Docker Hub returns 429 Too Many Requests, and whatever was pulling — a CI job, a pod scheduling onto a fresh node, a production rollout — fails.
The thesis of this post is simple: for a mid-size team running ephemeral CI and an autoscaling cluster behind one egress IP, the worst 6-hour window of a normal workday lands between 100 and 250 pulls. That straddles both free-tier limits. Let's show the work.
A Busy Team's Daily Pull Ledger
Take a concrete, unexceptional setup: 15 developers, one main application, self-hosted CI runners, and a 12-node Kubernetes cluster — everything egressing through a single IPv4 address.
The CI runners are ephemeral — fresh VMs or fresh Docker-in-Docker environments per job, which is the standard isolation posture in 2026. Ephemeral runners are the pull amplifier: with no persistent local layer cache, every job re-pulls every image it needs.
A typical CI run for this team touches Docker Hub four times:
| Image | Role | Pulls per run |
|---|---|---|
node:22-alpine | Build stage base | 1 |
nginx:1.27-alpine | Runtime stage base | 1 |
postgres:16-alpine | Integration-test service | 1 |
redis:7-alpine | Integration-test service | 1 |
| Total | 4 |
Fifteen developers on merge trains generate about 40 CI-triggering events per working day — pushes, PR updates, merges, the occasional retry. Each PR also spins up a preview environment in the cluster, whose pods pull postgres:16-alpine and redis:7-alpine onto whichever node they schedule to — call it 2 more pulls per event, since a preview rarely lands on a node that already has the images.
Daily total: 40 × (4 + 2) = 240 pulls. Against a limit measured per rolling 6-hour window, the daily total matters less than the shape of the day — and workdays are front-loaded. A standup-then-merge-train rhythm concentrates roughly 60% of daily CI activity into the 6-hour window from mid-morning to mid-afternoon:
Worst 6-hour window: ~24 events × 6 pulls = 144 pulls.
Now compare that to the table above:
- Unauthenticated (100 per 6h per IP): you're over the limit every single day. The 429s start mid-morning, exactly when everyone is trying to merge.
- Authenticated Personal (200 per 6h): you survive — at 72% utilization, with no headroom for anything unusual.
And "unusual" is Tuesday. Two common events blow the authenticated budget:
- Multi-arch builds. Ship for amd64 and arm64 — Graviton nodes, Apple-silicon dev machines — and the two base-image pulls per run become four. The worst window becomes 24 × (8 + 2) = 240 pulls. Over the Personal limit.
- Re-run storms. A shared dependency bump or a flaky-test epidemic re-triggers CI across every open PR: 60 jobs in three hours is 360 pulls before lunch.
The Kubernetes Multiplier
The cluster adds its own line items, and they arrive in bursts:
- Node churn. The autoscaler adds a node; its containerd starts cold and pulls every DaemonSet image plus whatever pods land there — typically 6–10 Hub pulls per fresh node. Three scale-ups in an afternoon: ~25 pulls.
- Rolling upgrades. A cluster upgrade that replaces all 12 nodes re-pulls the full working set on each: 12 × 8 ≈ 100 pulls in one operation — an entire unauthenticated window's budget spent on a routine upgrade.
Kubernetes nodes don't share image caches with each other. Every node pulls independently, and they all share your one NAT'd bucket with CI.
Where the Threshold Actually Sits
The variable that drives everything is pull events per peak 6-hour window through one egress IP. Run the sensitivity across three team profiles:
| Profile | Setup | Worst 6-hour window | Verdict |
|---|---|---|---|
| Solo / small team | ≤10 CI runs/day, persistent runner with a warm layer cache | 10–30 pulls | Fine on free tiers. Cache is an optimization. |
| Busy CI team (our example) | 40 runs/day, ephemeral runners, preview envs | ~144 pulls; 240+ with multi-arch or re-run storms | Over the unauthenticated limit daily; over Personal on any bad day. Cache is a reliability requirement. |
| Fleet scale | Autoscaling cluster + ephemeral CI + 10+ nodes, one egress IP | 200–400+ pulls; a single rolling upgrade ≈ 100 | Both free tiers are unusable. Cache (or paid seats for every pulling identity) is mandatory. |
You can compute your own tipping point directly:
Tipping point (authenticated): 200 ÷ (pulls per CI event) = events per 6 hours. At 6 pulls per event, that's 33 events per 6 hours — about 55 events/day with a front-loaded schedule. A 15-developer team on merge trains crosses it on any above-average day. At 10 pulls per event (multi-arch), the ceiling drops to 20 events per 6 hours — a 10-developer team crosses that.
There's also a non-numeric threshold that usually decides the question first: the day a 429 blocks a production deploy. A rollout that can't pull nginx:1.27-alpine onto a replacement node because CI spent the pull budget an hour earlier is a coupling between your deploy path and a third party's rate limiter. Paying Docker for Pro seats removes the number but not the coupling — the dependency, and the pricing policy risk the 2025 episode demonstrated, remain.
Once any 6-hour window can exceed your bucket — or one 429 can stall a deploy — the cache stops being optional. So let's build one.
Two Ways to Run the Cache
Option 1: A Bare Pull-Through Cache
The Docker registry:2 image has a proxy mode built in. One config file:
# /etc/docker/registry/config.yml
proxy:
remoteurl: https://registry-1.docker.io
username: your-hub-user # optional; use an access token
password: your-hub-tokenPoint every Docker daemon at it:
{
"registry-mirrors": ["https://mirror.internal.example.com"]
}First pull of an image goes upstream (spending Hub pulls once, authenticated as the mirror's account); every subsequent pull from every machine is served from the cache at LAN speed and costs zero Hub pulls. The cache periodically evicts stale content to bound disk usage.
Limitations to know before choosing it: it mirrors Docker Hub only (one upstream per instance), it's read-only (no pushes), and if you configure Hub credentials, anything that account can access is served to anyone who can reach the mirror — put authentication in front of it.
Option 2: A Harbor Proxy-Cache Project
Harbor — the CNCF-graduated registry — offers proxy-cache projects: create a registry endpoint for Docker Hub, create a project with the Proxy Cache toggle on, and pull through it as harbor.internal/dockerhub-proxy/library/node:22-alpine.
Harbor earns its extra weight in three ways:
- Rate-limit-aware freshness checks. Since v2.1.1, Harbor revalidates cached content with HEAD requests, which don't count as pulls. A cache hit costs zero Hub budget even while staying current; only genuinely changed images trigger a real pull.
- Sensible retention out of the box. New proxy-cache projects get a 7-day retention policy automatically, and you can tune tag-retention rules per project.
- Multiple upstreams and real access control. One Harbor instance can proxy Docker Hub, GHCR, Quay, and GCR as separate projects, with per-project RBAC, robot accounts for CI, and vulnerability scanning — and it also hosts your own images, which the bare cache can't.
Which to pick: if the only problem is Hub rate limits, registry:2 in proxy mode is an afternoon's work and near-zero operational surface. If you're running a fleet and will inevitably want a private registry, scanning, and multi-upstream caching, deploy Harbor once and solve all of it.
Sizing the Box
The cache's working set is distinct images, not pull volume — that's why the numbers are small. Our example team's ledger, deduplicated, is 4 base/service images plus a long tail of tool images, CI helpers, and version-skewed tags. A realistic catalog:
| Component | Estimate |
|---|---|
| Distinct images in weekly use | ~50 tags |
| Average compressed size (Alpine-heavy mix) | 100–300 MB |
| Working set | 15–25 GB |
| Tag churn (weekly patch bumps, 7-day retention) | ~2× headroom |
| Disk to provision | 50–100 GB |
Compute is an afterthought for the bare cache — registry:2 idles under 256 MB of RAM and serves a small team on a fraction of a core. Harbor's component set (core, postgres, redis, trivy, registry) wants about 2 vCPU and 4 GB RAM. Either way, this is a small VM or a corner of the management cluster you already run — not a capacity-planning project. The team above, at 240 pulls/day, sees well over 95% of pulls become LAN-speed cache hits; upstream traffic drops to a handful of genuinely-new-tag pulls per day, comfortably inside even the unauthenticated bucket.
The side effect you'll notice first isn't the absent 429s — it's CI speed. A 150 MB base image arrives in about a second on a local network instead of crawling in from Hub's CDN, on every one of those 40 ephemeral runs.
Wiring It Into a Cluster API Fleet
A cache only counts if every node actually uses it — including the node the autoscaler creates at 3 a.m. On a Cluster API fleet, mirror configuration belongs in the node bootstrap template, not in a runbook. Containerd's registry-host config is a file:
# /etc/containerd/certs.d/docker.io/hosts.toml
server = "https://registry-1.docker.io"
[host."https://mirror.internal.example.com"]
capabilities = ["pull", "resolve"]Ship it in your KubeadmConfigTemplate under files:, and every machine the fleet provisions — scale-ups, replacements, upgrade rollouts — is born pointing at the cache. The 100-pull rolling-upgrade bill from earlier becomes ~8 upstream pulls for the first node and cache hits for the other eleven. No per-node configuration, no drift, and no dependency on someone remembering the mirror exists.
This is the pattern worth generalizing: on owned infrastructure, rate-limit exposure is a provisioning-template problem, solved once. Bex.co, the open-source AI-native Render alternative, takes exactly this approach — its Cluster API-provisioned nodes can bake registry-mirror configuration into the bootstrap spec, so every machine in the fleet inherits the cache from birth and a git push never stalls on a third party's pull budget.
The Walk-Back Is a Warning, Not a Reprieve
Docker reversed the 10/40 limits, cancelled pull-consumption charges, and shelved storage billing — genuinely developer-friendly moves. But the episode revealed the mechanism: the ceiling on free pulls is a policy variable, adjustable by announcement, and your CI pipeline and cluster autoscaler sit underneath it. The enforced limits are already tight enough that an ordinary 15-developer team brushes them on normal days and breaches them on bad ones.
The fix costs 50–100 GB of disk and an afternoon: a pull-through cache if you want minimal surface, Harbor if you want a real registry while you're at it, and a bootstrap-template entry so the whole fleet uses it automatically. After that, Docker Hub's next pricing announcement is something you read about — not something that pages you.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.
Sources
- Docker Hub usage and limits — Docker Docs
- Docker Hub pull usage and rate limits — Docker Docs
- Revisiting Docker Hub Policies: Prioritizing Developer Experience — Docker Blog
- Announcing Upgraded Docker Plans (November 2024) — Docker Blog
- Registry as a pull-through cache — Docker Docs
- Configure proxy cache — Harbor Docs



