Fix GitHub Actions Cache Not Found: Root Causes and Minimal Checks
Quick Answer
- Conclusion: GitHub Actions cache misses are almost always caused by unstable cache keys, incorrect cache paths, or branch scope restrictions—not by GitHub infrastructure issues.
- First checks: Verify your cache key uses
hashFiles()on a committed lock file (e.g.,package-lock.json,requirements.txt), confirm the cache path matches the tool's actual cache directory, and ensure the cache was saved on the default branch. - Minimal fix: Use the official
setup-*actions (e.g.,setup-node,setup-python) with their built-incacheinput instead of manually configuringactions/cache. - Environment boundary: Applies to all GitHub-hosted runners (Ubuntu, Windows, macOS); cache scope is per-branch (except the default branch), with a 7-day inactivity eviction and a 10GB total repository limit.
What Problem It Solves
GitHub Actions caching speeds up CI/CD workflows by reusing dependency installation artifacts across runs. The actions/cache action saves and restores directories like package manager caches, but misconfiguration leads to the frustrating Cache not found for input keys error. This article covers the root causes and provides concrete fixes for cache misses, unstable keys, and ineffective cache paths.
When This Error or Setup Appears
The cache-not-found error appears in these typical scenarios:
| Scenario | Symptom |
|---|---|
| PR builds | Cache saved on a feature branch is unavailable to other branches |
| Lock file regenerated | hashFiles() produces a different hash every run |
| Wrong cache path | Cache restores successfully but dependencies still reinstall |
| Low-traffic repo | Cache evicted after 7 days of inactivity |
| Cache size limit | Repository exceeds 10GB total cache storage |
Minimal Working Configuration
The simplest reliable approach is to use the official setup-* actions, which handle cache keys and paths automatically:
YAMLname: CI on: push: branches: [main] pull_request: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Node.js with cache uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Install dependencies run: npm ci - name: Run tests run: npm test
For Python projects:
YAML- name: Set up Python with cache uses: actions/setup-python@v5 with: python-version: '3.12' cache: 'pip' - name: Install dependencies run: pip install -r requirements.txt
For Java/Gradle:
YAML- name: Set up Java with cache uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '17' cache: 'gradle'
If you must use actions/cache manually, this is the minimal correct pattern:
YAML- name: Cache npm uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node-
Root Cause Analysis
1. Unstable Cache Keys
The most common cause of cache misses. The cache key must be deterministic—it should change only when dependencies change.
Bad key (changes every run):
YAMLkey: ${{ runner.os }}-node-${{ github.sha }}
Good key (changes only when lock file changes):
YAMLkey: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
Gotcha: If a workflow step modifies the lock file before the cache step (e.g., running npm install instead of npm ci), the hash changes on every run. Always commit lock files and use npm ci or pip install -r requirements.txt without regenerating them.
2. Incorrect Cache Path
Caching the wrong directory makes the cache useless even when it hits.
| Package Manager | Correct Cache Path | Wrong Path (Common Mistake) |
|---|---|---|
| npm | ~/.npm | node_modules |
| pip | ~/.cache/pip | site-packages |
| Gradle | ~/.gradle/caches | build/ |
| Go | $(go env GOCACHE) | vendor/ |
Caching node_modules fails because npm ci deletes it before installing, and it's not portable across OS/Node versions.
3. Branch Scope Restrictions
GitHub caches are scoped to the branch where they were saved. PR builds can only read caches from the base branch (e.g., main), not from the feature branch. If you only push to feature branches without merging, caches never populate for PRs.
Fix: Ensure the default branch runs the workflow on push so caches are saved there:
YAMLon: push: branches: [main] pull_request:
4. Cache Eviction and Limits
- Caches not accessed for 7 days are evicted.
- Total repository cache storage is limited to 10GB (per-repo, not per-branch).
- When the limit is reached, the oldest caches are removed.
For low-traffic repositories, consider a scheduled workflow to keep caches warm:
YAMLon: schedule: - cron: '0 0 */5 * *' # every 5 days
Common Errors and Fixes
Error: Cache not found for input keys: Linux-node-abc123def456...
Cause: The key doesn't match any saved cache. Either the key is unstable, or the cache was never saved on the accessible branch.
Fix:
- Check if
hashFiles()references a file that gets regenerated during the workflow. - Confirm the cache was saved on the default branch.
- Add
restore-keysfor partial matching:
YAMLkey: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node-
Error: Cache restored successfully but node_modules is still empty
Cause: The cache path doesn't match where the tool actually stores data.
Fix: Use the tool's global cache directory, not the project's install directory:
YAML# Wrong path: node_modules # Correct path: ~/.npm
Error: Cache key changes on every run despite no dependency changes
Cause: A step before the cache step modifies the lock file.
Fix:
- Verify the lock file is committed to the repository.
- Use
npm ciinstead ofnpm install(orpip install -r requirements.txtwithout--upgrade). - Add a debug step to print the hash:
YAML- name: Debug hash run: echo "Hash: ${{ hashFiles('**/package-lock.json') }}"
Production Notes and Security Checks
Cache Key Requirements
- Base the key on immutable files (lock files), never on timestamps or commit SHAs.
- Use
restore-keysto fall back to the most recent compatible cache. - Include the runner OS in the key to avoid cross-platform mismatches.
Security Considerations
- Caches are accessible to all branches of the same repository—never store tokens, secrets, or credentials in cached content.
- If your workflow uses
pip install -r requirements.txt, ensure the requirements file doesn't contain private package URLs with embedded credentials. - Consider using
actions/cachewithenableCrossOsArchive: trueonly when you explicitly need cross-OS cache sharing (rare).
Performance Tips
- Cache the package manager's global cache, not the installed dependencies.
- For monorepos, split caches by subproject to avoid invalidating the entire cache when one dependency changes.
- Monitor the repository's Caches page (Settings → Actions → Caches) to see hit rates and eviction activity.
FAQ
Q: Why does my GitHub Actions cache always miss?
A: The most common causes are: (1) an unstable cache key—hashFiles() references a file regenerated during the workflow; (2) an incorrect cache path—you cache node_modules instead of ~/.npm; (3) branch scope—PR builds can only read caches from the base branch; (4) eviction—the cache was inactive for 7 days or the 10GB repo limit was exceeded. Check the cache key stability first, then verify the path and branch scope.
Q: Should I cache node_modules or ~/.npm?
A: Cache ~/.npm (the npm global cache), not node_modules. node_modules is not portable across operating systems or Node versions, and npm ci deletes it before reinstalling, making the cache useless. Caching ~/.npm lets npm ci install dependencies from the local cache, dramatically speeding up builds.
Q: How do I debug whether the GitHub Actions cache hit or missed?
A: Enable ACTIONS_STEP_DEBUG by setting it to true in the workflow or repository secrets to see detailed cache lookup logs. In the GitHub UI, go to the repository's Actions → Caches page to view saved caches and their last-used timestamps. You can also add a debug step to print the computed cache key: run: echo "Cache key: ${{ hashFiles('**/package-lock.json') }}".