GitHub Actions: "You have reached your unauthenticated pull rate limit"
Docker Hub refused an image because too many anonymous pulls had come from the runner's address. The part specific to GitHub Actions is where the pull happens. The images for services:, container: and Docker actions are pulled before your first step runs, so a docker/login-action step does nothing for them. They need credentials in the workflow itself, or the same image from another registry.
What it looks like
Error response from daemon: toomanyrequests: You have reached your unauthenticated pull rate limit. ##[error]Docker pull failed with exit code 1
- For a service or a container job, the runner tries the pull three times, a few seconds apart, and then fails the job in Initialize containers, before any step (ContainerOperationProvider.cs). In one project that uses homeboy's workflow, every review job failed this way in three runs on October 9, 2026 (#17014), and in regelrecht four pull requests fell out of the merge queue (#1702).
- "Unauthenticated" in the message does not prove that no login was used. Docker Hub gives the same text to a logged-in Personal account that is over its own limit (docker/hub-feedback #2537, open).
How many pulls
Docker's documentation gives, per 6 hours: 100 for unauthenticated pulls, per IPv4 address or IPv6 /64 subnet; 200 for a Docker Personal account; no limit for Pro, Team and Business. The headers Docker Hub sent for an anonymous check on November 3, 2026 said ratelimit-limit: 100;w=3600, which is 100 per hour. Docker's documentation also notes that a third-party platform may use the same address to pull for many users, and homeboy's report puts its failures down to runners sharing addresses. To see the numbers from the job itself, with a HEAD request that does not count as a pull:
- run: |
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
1. Where each pull happens
services:andcontainer:: in Initialize containers, before the steps. Give themcredentials:(2).- Docker actions (
uses: docker://…, or an action whoseaction.ymlruns an image or a Dockerfile): in steps the runner adds before yours, named "Pull …" or "Build …" (ActionManager.cs). There is no setting for credentials. Use a copy of the image from another registry (3), or run the image yourself withdocker runafter a login step. docker pull,docker run,docker composeanddocker buildin your own steps, includingdocker/build-push-action: adocker/login-actionstep before them works.
2. Log in where it counts
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
steps:
- uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- run: docker compose up -d
- Use a Docker Hub personal access token with read-only access as the password.
- Runs for pull requests from forks get no secrets, and Dependabot's runs get only Dependabot secrets. There both
credentials:values are empty, and the workflow schema does not accept an emptyusernameorpassword: the job fails at Set up job, before any step, with "The template is not valid ... Unexpected value ''" (eshu #7948). curia wrote each block as one expression that gives an empty mapping when the secrets are missing (#2158); those runs then pull anonymously and count against the address ("The template is not valid"):credentials: ${{ secrets.DOCKERHUB_USERNAME && secrets.DOCKERHUB_TOKEN && fromJSON(format('{{"username":{0},"password":{1}}}', toJSON(secrets.DOCKERHUB_USERNAME), toJSON(secrets.DOCKERHUB_TOKEN))) || fromJSON('{}') }} - One Personal account shared by many jobs can run out too. A paid Docker plan has no pull limit.
3. The same image from another registry
public.ecr.aws/docker/library/postgres:16: the Docker Official Images on Amazon ECR Public. regelrecht fixed its service with this one-line change (#1702).mirror.gcr.io/library/postgres:16: Google's documentation says Artifact Registry "caches frequently-accessed public Docker Hub images on mirror.gcr.io". An image that few people pull may not be there.- Your own copy on
ghcr.io, pulled withcredentials:set togithub.actorandsecrets.GITHUB_TOKEN, withpackages: read. homeboy proposed this for its MySQL service (#17014).
Both public copies above answered for postgres:16 on November 3, 2026. If you pin images by digest, check that the copy has the same digest.
Telling it apart
429 Too Many Requestswith no further text: Docker Hub's abuse limit, counted per address for every user, logged in or not. Logging in does not change it; wait and retry.pull access denied for …, repository does not exist or may require 'docker login': the name is wrong or the image is private.- The same message in Bitbucket Pipelines has other places to log in: Bitbucket Pipelines and Docker Hub's limit.
See failed jobs from IntelliJ IDEA
Pipeline Inbox for GitHub Actions Workflows, a plugin by Lesstep for JetBrains IDEs 2024.1 or later, lists the runs of all your repositories in an IDE tool window, with their jobs and steps, and your current branch's runs are in the status bar. Reading is free; re-running failed jobs, cancelling, running workflows and reviewing deployments are Pro (US$1.50 a month, with a 30-day trial).
GitHub and GitHub Actions are trademarks of GitHub, Inc.; Docker and Docker Hub are trademarks of Docker, Inc.; IntelliJ IDEA is a trademark of JetBrains s.r.o. This page is not affiliated with, sponsored or endorsed by GitHub, Inc., Docker, Inc. or JetBrains.
More: ghcr.io "permission_denied: write_package" · "detected dubious ownership" · "No space left on device" · GitHub Actions plugins for IntelliJ IDEA compared · 日本語