GitHub Actions Cache Not Saved After an Exact-Key Hit
actions/cache skips the save when an entry already exists for the exact key, because cache entries are immutable. If the content changed but the key did not, the new content is never stored.
What this error means
The post-job cache step logs that the cache already exists and is not saved, so updated dependencies are never persisted under that key and later runs restore stale content.
Cache hit occurred on the primary key npm-abc, not saving cache.
# updated node_modules are not persisted because the key did not changeDiagnose it: was the cache hit, and was it the right one?
Cache bugs split into three shapes and they need different fixes: the cache never saved, it saved but the key never matches on restore, or it restored a stale entry through a restore-keys prefix and is now poisoning the build. The step output tells you which one you have.
- uses: actions/cache@v4
id: cache
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: What happened
run: |
echo "exact hit: ${{ steps.cache.outputs.cache-hit }}"
echo "key used: ${{ steps.cache.outputs.cache-matched-key }}"Common causes
Immutable cache keys
A cache key maps to one immutable entry. Once saved, the same key is never overwritten, so changed content under an unchanged key is lost.
Key not reflecting content changes
If the key does not include a hash of the inputs that actually changed, the new state cannot get its own entry.
How to fix it
Make the key reflect the content
Include a lockfile hash so a dependency change produces a new key and a fresh save, with restore-keys for fallback.
- uses: actions/cache@v4
with:
path: node_modules
key: npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-Use a rolling key when content changes often
- Add a build-cache style key that includes a date or counter when you genuinely want frequent refreshes.
- Rely on restore-keys to load the most recent prior entry.
- Avoid manual cache deletion as a workaround; fix the key instead.
Cache limits that produce confusing failures
- Repository cache is capped at 10 GB. Past that, GitHub evicts least-recently-used entries, so a large cache can silently stop persisting.
- Caches are scoped by branch. A cache written on a feature branch is not visible to another feature branch, only to its base and its own descendants.
- An entry not read for 7 days is evicted, so a rarely-run workflow effectively never has a warm cache.
- Restoring a cache built for a different tool version is worse than a cold start, because you get a corrupted tree instead of a clean install. Always include the tool version in the key.
How to prevent it
- Tie the cache key to a hash of the content it stores.
- Use restore-keys so a new key still restores a recent cache.
- Do not expect the same key to update; design keys to roll forward.