Description

Updatecli never downloads an image, yet it still consumes Docker Hub pull quota. This page explains why, and what to do about it.

The same mechanics apply to any registry that meters manifest requests; the numbers below are Docker Hub’s.

Rate limits

Docker Hub introduced pull rate limiting in November 2020. The allowance depends on how, and whether, you authenticate:

AccountPull limitWindow

Unauthenticated

100, per IPv4 address or IPv6 /64 subnet

6 hours

Personal (authenticated, free)

200

6 hours

Pro, Team, Business (paid)

Unlimited

-

The window is a rolling one, and for unauthenticated requests the counter is keyed to your network, not to you. A shared CI runner or an office NAT therefore pools everyone’s usage into one allowance of 100, which is why the limit is often reached far sooner than the number suggests.

Note
Verified against Docker’s pull documentation in July 2026. Docker has revised these limits before; treat the linked page as authoritative if it disagrees with this one.

Why Updatecli consumes quota

Docker counts manifest requests, not layer downloads, and it counts them by HTTP method, from Docker’s own definition:

  • A GET on a manifest emulates a real pull and counts.

  • A HEAD on a manifest does not count.

So Updatecli consumes quota without ever transferring an image, because checking a tag means asking for its manifest. Which method gets used depends on the plugin and the stage:

PluginStageRequestCounts?

dockerimage

source

tag listing

Not a manifest request

dockerimage

condition

GET

Yes

dockerdigest

source

GET

Yes

dockerdigest

condition

HEAD

No

Tip
A dockerdigest condition is the cheapest way to ask "does this tag still exist?". It is the one combination above that costs nothing. Reach for it in preference to a dockerimage condition when all you need is existence.
Warning

Narrowing a tagfilter or a versionfilter does not reduce quota consumption. Updatecli fetches the full tag list from the registry and filters it locally, so a tighter filter returns fewer results from the same number of requests.

To spend less quota, reduce how many resources query the registry, or how often the pipeline runs, not how much each one matches.

Staying under the limit

Authenticate

Authenticating doubles a free allowance from 100 to 200, and more importantly moves the counter from your shared network address to your account.

Create an account, then make the credentials available in one of two ways.

Ambient credentials, used by the dockerimage, dockerdigest, and helm resources. They read the standard Docker credential store, so a login is all that is needed:

docker login

That writes ~/.docker/config.json; DOCKER_CONFIG relocates it. See "Environment variables".

An auths block, supported by the autodiscovery crawlers that resolve images (dockerfile, dockercompose, kubernetes, helm, helmfile, flux, argocd, and others). Keys are registry URLs without a scheme, and each accepts either a token or a username/password pair:

autodiscovery:
  crawlers:
    dockerfile:
      auths:
        index.docker.io:
          username: '{{ requiredEnv "DOCKERHUB_USERNAME" }}'
          password: '{{ requiredEnv "DOCKERHUB_TOKEN" }}'

When auths is empty, those crawlers fall back to the ambient credentials as well. Each crawler page lists what it accepts, see "Dockerfile crawler" for an example.

Put a cache or proxy in front

A pull-through cache serves repeated requests without touching Docker Hub. Updatecli’s registry client honours the standard proxy variables, so HTTPS_PROXY and NO_PROXY apply to registry traffic as well as to plain HTTP:

export HTTPS_PROXY="http://proxy.example.com:3128"
export NO_PROXY="ghcr.io,internal.example.com"

GitLab’s dependency proxy write-up is a worked example of the pattern. The alternative is to point manifests at a mirror registry directly, which costs no Docker Hub quota at all.

Do not retry on failure

When Updatecli runs as a Kubernetes CronJob, a retry after a rate-limit failure spends quota it does not have and pushes the reset further away. Turn retries off:

spec:
  jobTemplate:
    spec:
      backoffLimit: 0
      template:
        spec:
          restartPolicy: Never

The same reasoning applies to any scheduler: a rate-limit failure is not transient within the window, so retrying inside it cannot succeed.

Checking your current usage

Docker returns the quota state in response headers, described under View pull rate and limit.

To read it as an anonymous consumer over IPv4:

# Retrieve an authentication token for the API.
TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq -r .token)

# Retrieve the quota state using that token.
curl -4 --head -H "Authorization: Bearer $TOKEN" \
  https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest

Three headers matter:

ratelimit-limit

Your allowance and its window, as 100;w=21600 (100 requests per 21600 seconds).

ratelimit-remaining

What is left in the current window.

docker-ratelimit-source

The address or account the counter is keyed to. This is how to confirm whether you are being counted per network or per account.

Repeat with curl -6 to check IPv6, the two are metered independently, so a host with both may show plenty of quota on one and none on the other.

If you authenticated with docker login, Docker’s documentation covers retrieving the figures for the account rather than the address.

Go Further