GitHub Actions cache never restores because key includes github.run_id
GitHub Actions cache never restores because key includes github.run_id
A build that should reuse dependencies starts from scratch on every push. The cache step reports a miss, the install step downloads the same packages again, and CI time and billed minutes climb. The usual cause is a cache key that contains github.run_id. Because that value changes on every workflow run, the exact key changes too, so actions/cache never finds the previous entry unless a restore key happens to match.
Why github.run_id breaks cache matching
actions/cache restores a cache by matching the key exactly. If no exact match exists, it tries restore-keys as prefixes in order. github.run_id is unique per workflow run [link needed]. A key like this never repeats:
key: deps-${{ runner.os }}-${{ github.run_id }}-${{ hashFiles('package-lock.json') }}
The lockfile hash is stable, but the run ID in the middle means the full key is different every time. Without restore-keys, every run is a guaranteed miss. Even with restore keys, a prefix ending before the run ID saves you; a prefix that includes the run ID does not.
How to fix a GitHub Actions cache key run_id problem
Use a numbered procedure:
1. Find the cache step in the workflow and print the key it generates. Run the workflow with a debug step or inspect the key: expression directly.
2. Remove github.run_id from the exact key unless you need per-run isolation. Use a stable key based on the lockfile and OS:
```yaml
key: deps-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
```
3. If you want the run ID for traceability, put it at the end and add restore-keys that stop before it:
```yaml
key: deps-${{ runner.os }}-${{ hashFiles('package-lock.json') }}-${{ github.run_id }}
restore-keys: |
deps-${{ runner.os }}-${{ hashFiles('package-lock.json') }}-
```
4. Keep the cache path narrow. Broad paths make a bad key more expensive because every miss writes a large archive.
5. Re-run the workflow and confirm the log says the cache was restored, not just saved.
Verify the restore instead of assuming it
Check the cache step log for a line beginning with Cache restored from key or a fallback message showing which restore key matched [link needed]. If you see Cache not found for input keys, the key or prefix is still wrong. Also compare save and restore events over two consecutive runs: the second run should restore what the first saved, unless the dependency hash changed.
A quick local check: copy the key expression into a scratch workflow, run it twice without changing dependencies, and compare the resolved keys in the logs. If they differ and no restore key covers the stable prefix, you have reproduced the bug.
When NOT to do this
Do not remove github.run_id from the key if the cache must be immutable per run. Examples include build outputs that are consumed by later jobs only within the same run, or caches you deliberately isolate to avoid stale artifacts after a bad dependency state. In those cases, accept the miss and write a fresh cache. Also leave the run ID out of restore-keys only when you are comfortable restoring an older entry; if you need strict identity, use the exact key and skip restore keys entirely.
Checklist
- [ ] Cache key does not contain
github.run_idunless per-run isolation is required. - [ ] If
github.run_idis present, it is at the end of the key. - [ ]
restore-keysend before the run ID. - [ ] The cache path is limited to dependency directories.
- [ ] Two consecutive runs show a restore, not a save-only cycle.
- [ ] Cache logs are checked after the first fix, not assumed.