Skip to content
Latchkey LogoLatchkey home

error writing layer blob: failed to reserve cache

"error writing layer blob: failed to reserve cache" in GitHub Actions is the buildx gha cache exporter being refused when it asks the Actions cache service to reserve an entry. The image was usually built and pushed already; what failed is the cache export, and the fix is to stop exporting from the jobs that either may not write or are racing each other for the same key.

Diagram of the buildx gha cache export being refused, and the three causes behind it
The export is the last thing a build does, which is why the failure arrives after the image is already pushed.

What this error means

The build log looks like a success until the last few seconds. The image is built, the push completes, and then the step fails on the export with "ERROR: failed to solve: error writing layer blob: failed to reserve cache". Sometimes one blob is written before it stops, which is what makes it look like a quota problem. The three things people check first come back clean: the cache is nowhere near its limit, the permissions are right, and the same job passed yesterday. It is intermittent on a matrix and consistent on a fork pull request, and those two shapes have different explanations. The block below is quoted from the canonical issue rather than from a run of ours, and the section on reproduction says why.

Quoted from docker/build-push-action#1571
ERROR: failed to solve: error writing layer blob: failed to reserve cache

The build succeeded; the export did not

cache-to: type=gha is not part of the build. It runs once the image exists, taking each layer blob and asking the Actions cache service to reserve an entry before uploading. The reservation is what gets refused, and because it happens at the end, the log reads like a build failure arriving after the build worked.

That ordering is also the useful diagnostic. If your image is in the registry and the step is red, you are looking at a cache export problem and not at a broken Dockerfile, which rules out most of what a search will suggest.

Common causes

The workflow has read-only cache access

The most common cause and the most deterministic. Events such as pull_request_target and issue_comment get read-only access by default, so a build under one can read a cache and cannot reserve an entry. It fails identically every run, which is how you tell it from the matrix race.

You raised the limits and added permissions, and it kept failing

The wasted fix, and the reason the canonical issue runs as long as it does. Its reporter confirmed the cache was well inside the limit, added actions: write, tested two major versions of the action and checked the toolchain, and none of it changed the outcome. Quota and permissions are the first two guesses and usually not the answer.

Two matrix legs reserve the same key at the same time

Layer blobs are keyed by digest, ref and repository, and legs that share a base image share those digests. When two exports collide the second reservation is refused. It is intermittent by nature, and adding scope= does not separate them because the scope is not part of the reservation key.

The toolchain predates the cache service v2 cutover

Since April 2025 only the v2 cache API is served, and Buildx, BuildKit, Compose and Engine each have a minimum version that speaks it. A pinned action or a self-hosted image that has not moved will fail on the export even though everything about the workflow is correct.

How to fix it

Import everywhere, export from one place

  1. Remove cache-to from pull request and comment-triggered workflows, and leave cache-from in place.
  2. Populate the cache from the push workflow on your default branch, where write access exists.
  3. Verify by checking that the cache entries appear after a merge rather than after a pull request build.
.github/workflows/ci.yml
# pull_request: read the cache, never write it
- uses: docker/build-push-action@v7
  with:
    cache-from: type=gha

# push to the default branch: write it
- uses: docker/build-push-action@v7
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max

Stop matrix legs from exporting the same layers

Export from one leg rather than all of them, or give the legs genuinely different base layers. Exporting from a single leg keeps the benefit, because the layers a matrix shares are exactly the layers the other legs will import next time.

.github/workflows/ci.yml
- uses: docker/build-push-action@v7
  with:
    cache-from: type=gha
    cache-to: ${{ matrix.node == 22 && 'type=gha,mode=max' || '' }}

Bring the toolchain up to the v2 minimums

Update setup-buildx-action and the build action to current majors, and on a self-hosted image check the engine version too. This is the cause with an unambiguous test, so it is worth clearing before you rearrange any workflows.

.github/workflows/ci.yml
- uses: docker/setup-buildx-action@v4
  with:
    version: latest
- run: docker buildx version

Use ignore-error only once you know which cause you have

The cache exporter accepts ignore-error=true, which turns the export failure into a warning. It is a reasonable last line for a workflow you have already decided should not be writing, and a bad idea before that, because it suppresses every cache export error and not just this one. Docker says the same in their own documentation.

.github/workflows/ci.yml
- uses: docker/build-push-action@v7
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max,ignore-error=true

Read-only cache access is a documented state, not a bug

Some events get read-only access to the cache by default, issue_comment and pull_request_target among them, and a build in one of those can import a cache and cannot export to one. Docker documents the failure this produces by name, saying the export can fail with "error writing layer blob: failed to reserve cache" when write access is restricted.

Their recommendation is the fix rather than a workaround. Drop cache-to from the workflows that may not write, and populate the cache from one that can, such as a push workflow on the default branch. Every other job then reads a cache somebody else filled, which is what you wanted anyway.

.github/workflows/ci.yml
- uses: docker/build-push-action@v7
  with:
    push: false
    cache-from: type=gha
    # no cache-to here: this workflow may not write

# and in the push workflow on the default branch
- uses: docker/build-push-action@v7
  with:
    push: true
    cache-from: type=gha
    cache-to: type=gha,mode=max

On a matrix, two jobs ask for the same entry

The other shape is a race. The exporter keys an entry per layer blob by its digest together with the git ref and the repository, and the scope= parameter people reach for is not part of that key. Matrix legs sharing base layers therefore reserve identical keys, and when two arrive together the second is refused.

That is why the failure is intermittent in exactly the way a race is: it depends on which legs reach the export phase at the same moment, so it tracks runner availability rather than anything in your build.

Check the versions before anything else

Docker's documentation records that as of 15 April 2025 only cache service API v2 is supported, and lists the minimums that speak it: Buildx v0.21.0, BuildKit v0.20.0, Compose v2.33.1 and Engine v28.0.0. A toolchain under those cannot use the gha backend.

Rule this out first, because it is the one cause with a definite answer. Print the versions once and compare them against that list: either the toolchain is fine, or you have found it.

.github/workflows/ci.yml
- run: |
    docker buildx version
    docker version --format "engine {{.Server.Version}}"
    docker buildx inspect --bootstrap | grep -i buildkit

What the runner does about it, which today is nothing

This is the one page in the cluster where the honest answer is that the engine has no entry for the failure. We checked every regex in the pattern library against this string and none matches: it is not a registry rate limit, not a DNS failure, not a git transport error, and the cache-service pattern is anchored to the actions/cache wording rather than BuildKit's. The step fails, the job reports it, and nothing is repaired.

There is no reproduction on this page either, for the same reason the cache service 503 page has none: the gha cache backend answers only inside a GitHub Actions job, so a Latchkey runner cannot ask it for a reservation at all. A stub that refused one would prove nothing about the real service. The candidate detection this failure deserves has been passed to the engine team; until it exists, the page says so rather than implying otherwise.

How to prevent it

  • Write the cache from one workflow, on the default branch, and read it everywhere else.
  • Keep cache-to off every event that has read-only cache access.
  • Export from one matrix leg rather than from all of them.
  • Pin the build actions to current majors so the v2 cache API minimums stay met.

Frequently asked questions

What does failed to reserve cache mean in buildx?
It means the gha cache exporter asked the Actions cache service to reserve an entry for a layer blob and was refused. The build already finished, so the image may well be pushed. The two common reasons: the workflow may not write to the cache, or another job reserved the identical key first.
Does cache-to type=gha work on pull requests from forks?
Not reliably, and by design. Several events get read-only cache access, so the export is refused while the import still works. Read the cache in those workflows and write it from a workflow on the default branch, which Docker recommends in their own documentation for exactly this error.
Should I use ignore-error=true on the gha cache?
Only once you know why the export is failing. It converts the failure to a warning, which is right for a workflow that was never supposed to write, and it hides every other cache export problem too. Fix the cause first, then use it to keep the log clean.
What replaced the GitHub Actions cache service v1 for buildx?
Cache service API v2, the only supported version since 15 April 2025. Docker lists the minimums that speak it: Buildx v0.21.0, BuildKit v0.20.0, Compose v2.33.1 and Engine v28.0.0. A toolchain below any of those cannot use the gha cache backend.

Related guides

References

The push succeeded and only the cache export failed. Latchkey keeps layers on the runner instead of exporting them. Start free → 30-day trial · No credit card