GitLab CI Error Guide: 'toomanyrequests: You have reached your pull rate limit' 429 Pulling Images
Fix GitLab CI 429 Too Many Requests and Docker Hub pull rate limits: use the Dependency Proxy, authenticate to Docker Hub, and cache or mirror images so CI jobs stop failing.
- #gitlab-cicd
- #troubleshooting
- #errors
- #registry
Exact Error Message
A job fails while pulling its image because Docker Hub returned HTTP 429 (rate limited):
Using docker image sha256:... for node:20 ...
ERROR: Job failed: failed to pull image "node:20":
toomanyrequests: You have reached your pull rate limit.
You may increase the limit by authenticating and upgrading:
https://www.docker.com/increase-rate-limit
The same condition appears in other forms depending on where the pull happens:
429 Too Many Requests
received unexpected HTTP status: 429 Too Many Requests
ERROR: Preparation failed: failed to pull image 'docker:27-dind':
toomanyrequests: You have reached your pull rate limit.
It is intermittent — the job passes on a re-run minutes later, then fails again under load. That flakiness is the signature of a shared-IP rate limit, not a broken image.
What the Error Means
Docker Hub enforces a pull rate limit per IP for anonymous users and per account for free authenticated users, measured over a rolling window. GitLab CI runners frequently pull base images (node, python, alpine, docker:dind) at the start of every job. On shared/hosted runners, many projects share a small pool of egress IPs behind NAT, so the collective pull rate from that IP blows past the anonymous limit and Docker Hub starts returning 429 / toomanyrequests to everyone on it.
So the failure usually has nothing to do with your project’s pull volume. You are sharing an IP’s anonymous quota with strangers. The durable fix is to stop hitting Docker Hub anonymously on every job — authenticate, or route pulls through a cache that pulls upstream once and serves everyone else locally.
Common Causes
- Anonymous pulls from shared runner IPs. Hosted/shared runners NAT many jobs through few IPs, exhausting Docker Hub’s per-IP anonymous limit.
- No Docker Hub authentication. Authenticated free accounts get a higher limit than anonymous; you are pulling logged-out.
- No image caching or mirror. Every job pulls fresh from Docker Hub instead of a pull-through cache or the GitLab registry.
docker:dindpulls images too. Inside dind,docker build/docker pullhit Docker Hub again — separate from the job image pull — doubling the rate.- Dependency Proxy not used. GitLab’s built-in pull-through cache for Docker Hub is available but not configured in the job.
- High-frequency pipelines. Many parallel jobs or frequent commits multiply pulls of the same base images.
How to Reproduce the Error
Run a fan-out of anonymous pulls of a Docker Hub image from a shared runner:
# .gitlab-ci.yml — many parallel jobs all pulling anonymously
pull-storm:
image: alpine:3.20 # pulled from Docker Hub, logged out
parallel: 40
script:
- echo "job $CI_NODE_INDEX"
Under load (or after other tenants on the same runner IP have consumed the quota), some jobs fail before the script runs with toomanyrequests: You have reached your pull rate limit. Re-running a single job often succeeds, confirming a rate limit rather than a bad image.
Diagnostic Commands
Confirm it is a Docker Hub rate limit (not a registry-auth or not-found error) and inspect the remaining quota:
# Get an anonymous token, then read the rate-limit headers from the manifest HEAD
TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq -r .token)
curl -s -I -H "Authorization: Bearer $TOKEN" \
"https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest" \
| grep -i ratelimit
ratelimit-limit: 100;w=21600
ratelimit-remaining: 0;w=21600
ratelimit-remaining: 0 over a 21600s (6h) window means the IP’s quota is spent. Check what the job is actually pulling:
# In the failing job log: which image and registry?
grep -i "pull image\|toomanyrequests\|429" job.log
# Confirm the source is Docker Hub (docker.io) vs your GitLab registry
docker pull node:20 ; echo "exit=$?"
echo "$CI_REGISTRY $CI_DEPENDENCY_PROXY_SERVER"
If the image reference has no registry host (e.g., node:20), it resolves to Docker Hub by default — that is the pull being throttled.
Step-by-Step Resolution
-
Route Docker Hub pulls through the GitLab Dependency Proxy (a pull-through cache). It pulls upstream once and serves cached layers to every job:
default: image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/node:20 build-image: image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:27 services: - name: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:27-dind before_script: - echo "$CI_DEPENDENCY_PROXY_PASSWORD" | docker login -u "$CI_DEPENDENCY_PROXY_USER" --password-stdin "$CI_DEPENDENCY_PROXY_SERVER" script: - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" . -
Authenticate to Docker Hub to get the higher per-account limit. Store a Docker Hub username and access token as masked, protected CI/CD variables and log in before pulling:
variables: DOCKER_AUTH_CONFIG: '' # or set via Settings > CI/CD > Variables before_script: - echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USER" --password-stdinTo authenticate the job image pull itself (not just inside the script), set the
DOCKER_AUTH_CONFIGCI/CD variable with a base64 Docker Hub credential so the runner pulls authenticated. -
Push base images to your GitLab Container Registry and pull from there.
CI_REGISTRYhas no Docker Hub limit:docker pull node:20 docker tag node:20 "$CI_REGISTRY_IMAGE/base/node:20" docker push "$CI_REGISTRY_IMAGE/base/node:20"test: image: $CI_REGISTRY_IMAGE/base/node:20 # served from GitLab, not Docker Hub script: ["npm test"] -
Configure a registry mirror / pull-through cache on the runner so all pulls from that runner go through your cache instead of Docker Hub directly (set
--registry-mirroron the dind service or in the runner’s Docker config). -
Pin and cache deliberately. Pin image digests so the cache stays warm, and avoid re-pulling identical base images across dozens of parallel jobs.
After switching to the Dependency Proxy or authenticated pulls, re-run the pipeline and confirm jobs no longer hit toomanyrequests.
Prevention and Best Practices
- Make the Dependency Proxy the default for every Docker Hub image via
CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX, including thedocker:dindservice — dind pulls count too. - Authenticate to Docker Hub with masked, protected CI/CD variables so anonymous shared-IP limits never apply.
- Mirror your handful of base images into the GitLab Container Registry and pull from
CI_REGISTRY_IMAGE. - Pin image digests to keep the pull-through cache warm and reproducible.
- Reduce needless pulls: reuse a single base image across jobs rather than many slightly-different ones, and avoid huge
parallel:fan-outs that all pull at once. - The free incident assistant can distinguish a
429rate limit from adenied/not foundregistry error and suggest the proxy/auth fix. More: GitLab CI/CD guides.
Related Errors
denied: requested access to the resource is deniedpushing toCI_REGISTRY— registry permission/auth, not a rate limit. See registry denied access on push.manifest unknown/not found— wrong tag or image path, distinct from429.unauthorized: authentication required— missing/invalid Docker Hub or registry login.error pinging docker registry— network/TLS to the registry, not a quota issue.- See the GitLab CI/CD guides.
Frequently Asked Questions
Why does the same pipeline fail intermittently with 429? Because hosted/shared runners NAT many jobs through a few IPs, and Docker Hub’s anonymous limit is per IP over a rolling window. When the shared IP’s quota is exhausted by all tenants, pulls fail; minutes later the window frees up and a re-run succeeds.
What is the GitLab Dependency Proxy and how does it help? It is a built-in pull-through cache for Docker Hub. The first pull fetches upstream and caches the layers; every later job pulls from the cache instead of Docker Hub, so you make one upstream request instead of thousands. Reference images via ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/<image>.
Does authenticating to Docker Hub fix it? It raises the limit (per-account instead of anonymous per-IP) and is often enough for moderate volume. For heavy CI, combine authentication with the Dependency Proxy or a GitLab registry mirror so you rarely touch Docker Hub at all.
Do I need to fix the docker:dind service too? Yes. The dind service image is pulled separately, and any docker pull/docker build inside it hits Docker Hub again. Pull the dind image via the Dependency Proxy and docker login inside the job so those pulls are cached/authenticated as well.
How do I check how many pulls I have left? Request an anonymous token for ratelimitpreview/test and do a HEAD on its manifest; the ratelimit-limit and ratelimit-remaining response headers show the quota and what is left for that IP/account over the window.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.