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.

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.
ERROR: failed to solve: error writing layer blob: failed to reserve cacheThe 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
- Remove
cache-tofrom pull request and comment-triggered workflows, and leavecache-fromin place. - Populate the cache from the push workflow on your default branch, where write access exists.
- Verify by checking that the cache entries appear after a merge rather than after a pull request build.
# 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=maxStop 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.
- 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.
- uses: docker/setup-buildx-action@v4
with:
version: latest
- run: docker buildx versionUse 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.
- uses: docker/build-push-action@v7
with:
cache-from: type=gha
cache-to: type=gha,mode=max,ignore-error=trueRead-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.
- 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=maxOn 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.
- run: |
docker buildx version
docker version --format "engine {{.Server.Version}}"
docker buildx inspect --bootstrap | grep -i buildkitWhat 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-tooff 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.