GitHub Actions: "Failed to save: Unable to reserve cache with key"
The save at the end of the job did not happen, and the job still passes. The line comes from the cache package of GitHub's actions toolkit, which actions/cache uses, and so do the caches built into setup-node, setup-go, setup-uv and others. Two different endings follow the same start, and they mean different things.
The two lines
Failed to save: Unable to reserve cache with key <key>, another job may be creating this cache.An entry with this key already exists, or another job is saving it right now. An entry cannot be replaced; GitHub's documentation says "You cannot change the contents of an existing cache. Instead, you can create a new cache with a new key." Inactions/cachethis is a plain log line, not a warning.Failed to save: Unable to reserve cache with key <key>. More details: cache write denied: token has no writable scopes, as a warning. This run was given a cache token that can only restore. GitHub started doing this on June 26, 2026, for runs started by events that people without write access can trigger.
The key already exists
From a push to main in pymovements on October 28, 2026 (a pull request to stop it: pymovements #1788):
Run actions/cache/save@v4 ... [command]/usr/bin/tar --posix -cf cache.tzst --exclude cache.tzst -P -C /home/runner/work/pymovements/pymovements --files-from manifest.txt --use-compress-program zstdmt Failed to save: Unable to reserve cache with key test-ubuntu-latest-py3.13-4ec5...c7b4, another job may be creating this cache. ##[warning]Cache save failed.
- The second warning is only in
actions/cache/save@v4. From v5 the save step adds nothing to the first line. - The archive is built before the key is reserved, so the job compresses the whole cache and then throws it away. A change that checks for the key first is proposed in actions/toolkit #2550 and #2556, not merged yet.
matlab-actions/run-buildlogs "Cache saved with key" right after the failure (run-build #91). Nothing was saved.
Where it usually comes from:
- Matrix jobs with the same key. The matrix varies something that is not in the key, so every job misses on restore and they all try to save; one wins. In schemachange #475 the setup-uv key had the Python version, but the matrix also varied the test scenario, so three jobs per Python version raced.
- Two jobs with the same setup step.
setup-nodewithcache: pnpmin both a web job and an end-to-end job computes the same key (grounded-expungement-eligibility-agent #110). - A separate
actions/cache/savestep that runs every time. After the first save of a key, every later run fails like this (pymovements #1788 above). - A key that never changes, with no
hashFiles()in it, is saved once. After that each save fails like this, oractions/cachelogs "Cache hit occurred on the primary key ..., not saving cache.", and the cache keeps what it had at its first save.
This is harmless when the jobs would save the same files: the job that won saved them, and later runs restore them. It is not harmless when the jobs build different files under one key, for example a matrix of compilers on the same OS. Then the other jobs restore files built for a different setup, so put that matrix value in the key.
Stopping it
Save only when the restore missed, and build the key from the files that decide the contents:
- uses: actions/cache/restore@v6
id: cache-restore
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-${{ hashFiles('requirements*.txt') }}
restore-keys: pip-${{ runner.os }}-
# ... install and test ...
- uses: actions/cache/save@v6
if: steps.cache-restore.outputs.cache-hit != 'true'
with:
path: ~/.cache/pip
key: ${{ steps.cache-restore.outputs.cache-primary-key }}
- In a matrix whose jobs share a key, let one job save and the others only restore. With
actions/cache, add a matrix test to the save step'sif:. With setup-uv, schemachange usedsave-cache: ${{ matrix.scenario-name == 'basics_demo' }}. - For two jobs that both use a setup action's cache, keep the cache in one of them and use
actions/cache/restorewith the same path and key in the other, or give each job its own key prefix.
This run may only restore
From a pull_request_target run on October 11, 2026 (sc4pac-actions #12):
##[warning]Failed to save: Unable to reserve cache with key sc4pac-download-cache-80c6c006f143. More details: cache write denied: token has no writable scopes
- The "Set up job" section of the same log says
Cache mode: read. A run that can save saysCache mode: write. A run started by an issue comment in provider-github #67 showed the sameCache mode: read. - By default, only
push,workflow_dispatch,repository_dispatch,delete,registry_package,page_buildandschedulecan create or replace caches in the default branch's scope. Other events that run on the default branch, such aspull_request_target,issue_commentandworkflow_run, can restore those caches but not save them.pull_requestis not affected: its caches belong to the pull request's merge ref. - Restoring still works, so the run is only slower when nothing was saved for it.
What GitHub suggests:
- Let a trusted workflow keep the cache filled, for example the build that runs on
pushto the default branch. The other workflow then restores what it saved. - In the low-trust workflow, restore only, with
actions/cache/restore, so there is no failed save to warn about.cache-mode: readon the job states the same thing in the workflow. cache-mode: writeon the job lets it save again, and the run then warns that it overrides the secure default. GitHub's documentation warns that this brings back cache poisoning, where untrusted code saves a cache that a more privileged workflow later restores. Use it only for a job that runs no code from forks or pull requests before saving.
jobs:
test:
runs-on: ubuntu-latest
cache-mode: read # read, write, write-only or none
steps:
- uses: actions/cache/restore@v6
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-${{ hashFiles('requirements*.txt') }}
The documentation also says the cache becomes read-only when a repository goes over the budget set for cache storage, until billing is resolved or usage drops under the free 10 GB.
Telling it apart
- "Cache not found for input keys: ..." is the restore step finding nothing. It is not an error, and the save at the end is what fills the cache.
- "Resource not accessible by integration": a different read-only token, the
GITHUB_TOKENof a run that lacks a permission ("Resource not accessible by integration").
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.; IntelliJ IDEA is a trademark of JetBrains s.r.o. This page is not affiliated with, sponsored or endorsed by GitHub, Inc. or JetBrains.
More: The "Unexpected input(s)" warning · "Artifact storage quota has been hit" · "Input required and not supplied" · "Cache not found for input keys" · GitHub Actions plugins for IntelliJ IDEA compared · 日本語