Your GitHub Actions caches are wasting money. You just can't see it.
Cache Doctor is a continuous linter and diagnostic tool for GitHub Actions cache. It finds the problems you didn't know you had -- orphan caches from deleted branches, bad key patterns that never hit, quota silently filling up -- and tells you exactly how to fix them.
Two lines of YAML. Zero infrastructure. Runs on every PR.
- uses: LeRedTeam/cache-doctor@v1
with:
mode: analyzeGitHub Actions cache has no dashboard, no hit/miss analytics, no quota alerts, and no eviction notifications. When your 10 GB quota fills up, GitHub silently evicts caches via LRU. Your builds go from 3 minutes to 15 minutes and nobody knows why.
You've been there:
- Notice builds are slow
- Wonder if it's cache-related
- Read raw workflow logs line by line
- Guess at the fix
- Wait for next builds
- Repeat
Cache Doctor replaces that loop with a single command.
Cache Health Report
==================================================
Quota: 7.2 GB / 10.0 GB (72%)
Total caches: 31
Active: 18 (3.8 GB)
Stale: 8 (2.1 GB) -- not accessed recently
Orphan: 5 (1.3 GB) -- from deleted branches
Issues found: 3
----------------------------------------
1. [HIGH] Quota pressure: 72% used
Cache quota at 72%. LRU eviction may be active.
2. [MEDIUM] 5 orphan caches from deleted branches waste 1.3 GB
These caches belong to branches that no longer exist.
3. [LOW] 8 stale caches not accessed recently waste 2.1 GB
These caches have not been accessed within the stale threshold.
Recommendations
----------------------------------------
1. [MEDIUM] Orphan caches from deleted branches
Found 5 orphan caches wasting 1.3 GB.
Action: cache-doctor cleanup --delete-orphans
Impact: Reclaim 1.3 GB
2. [LOW] Many stale caches
Found 8 stale caches wasting 2.1 GB.
Action: cache-doctor cleanup --stale-days 7
Impact: Reclaim 2.1 GB
Cache Cleanup Report
==================================================
Deleted 5 caches (1.3 GB reclaimed)
Orphan (1.3 GB):
- node-modules-Linux-feature/old-thing-abc123 (800.0 MB, branch: feature/old-thing)
- build-cache-feature/old-thing-def456 (200.0 MB, branch: feature/old-thing)
- deps-feature/experiment-ghi789 (300.0 MB, branch: feature/experiment)
Quota: 5.9 GB / 10.0 GB (55%) -- was 72%
Linting .github/workflows/ci.yml...
CD001 [WARNING] Line 24: No restore-keys defined. Cache is all-or-nothing.
> - uses: actions/cache@v4
Fix: Add restore-keys with a prefix fallback for partial cache matches.
CD011 [WARNING] Line 31: Cache key uses github.sha. This is unique per commit
so the cache will never hit.
> - uses: actions/cache@v4
Fix: Use hashFiles() on input files instead of github.sha.
CD016 [WARNING] Line 18: This job already uses a setup action with built-in npm
caching. This manual cache step may be redundant.
> - uses: actions/cache@v4
Fix: Remove this actions/cache step or disable built-in caching in the setup
action (cache: false).
CD017 [WARNING] Line 36: Restore-keys don't share a prefix with the cache key.
Key starts with "v2-build-" but no restore-key matches.
> - uses: actions/cache@v4
Fix: Ensure restore-keys use the same prefix as the cache key.
Found 8 issues (0 errors, 6 warnings, 2 info)
name: Cache Health
on: pull_request
permissions:
actions: read
jobs:
cache-doctor:
runs-on: ubuntu-latest
steps:
- uses: LeRedTeam/cache-doctor@v1
with:
mode: analyzeResults appear in the Job Summary tab of the workflow run.
name: Cache Cleanup
on:
schedule:
- cron: '0 2 * * 1' # Monday 2am
workflow_dispatch:
permissions:
actions: write
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- uses: LeRedTeam/cache-doctor@v1
with:
mode: cleanup
stale-days: '7'
delete-orphans: 'true'# Install
go install github.com/LeRedTeam/cache-doctor@latest
# Analyze
export GITHUB_TOKEN=ghp_...
cache-doctor analyze --repo your-org/your-repo
# Cleanup (always dry-run first)
cache-doctor cleanup --dry-run --delete-orphans --repo your-org/your-repo
cache-doctor cleanup --delete-orphans --repo your-org/your-repo
# Lint (Pro)
cache-doctor lint .github/workflows/Cache Doctor encodes the tribal knowledge that lives in scattered blog posts and Stack Overflow answers into automated checks.
| Rule | What It Catches |
|---|---|
| CD001 | Missing restore-keys -- cache is all-or-nothing, no partial matches |
| CD002 | Hardcoded cache key -- never invalidates, serves stale content forever |
| CD003 | Missing OS in key -- matrix jobs on different OS share incompatible caches |
| CD004 | Missing matrix vars in key -- Node 18 and Node 20 share the same cache |
| CD005 | Caching node_modules directly -- huge, platform-specific; use ~/.npm instead |
| CD006 | Overly broad hashFiles -- **/*.json changes constantly, busting cache |
| CD007 | No version prefix -- can't bulk-invalidate when cache format changes |
| CD008 | Path too broad -- caching ~ or . wastes quota on everything |
| CD009 | Duplicate cache steps -- same key twice in one job, wasting API calls |
| CD010 | Outdated actions/cache@v3 -- v4 has better performance |
| CD011 | github.sha in key -- unique per commit, cache never hits |
| CD013 | Restore-key too short -- bu matches ancient, irrelevant caches |
| CD014 | Reusable workflow cache without inputs -- callers collide |
| CD015 | Large paths (target/, .gradle, vendor/) -- wrong thing cached |
| CD016 | setup-node with cache: 'npm' plus manual actions/cache -- double caching |
| CD017 | Restore-key prefix mismatch -- v2-build-* key with build- restore, never matches |
| CD018 | Dynamic key without hashFiles -- ${{ runner.os }} alone never invalidates |
| CD019 | actions/cache/save without restore -- uploads every run, never downloads |
| CD020 | 6+ cache steps in one job -- caching strategy needs rethinking |
| Input | Default | Description |
|---|---|---|
token |
${{ github.token }} |
GitHub token |
mode |
analyze |
analyze, lint, or cleanup |
stale-days |
7 |
Days without access to consider stale |
delete-orphans |
false |
Delete caches from deleted branches |
dry-run |
false |
Show what would be deleted |
path |
.github/workflows/ |
Path to lint |
format |
summary |
summary, json, human, sarif |
top |
5 |
Number of largest caches to show |
| Output | Description |
|---|---|
quota-pct |
Current quota usage % |
total-caches |
Total caches |
stale-count |
Stale caches |
orphan-count |
Orphan caches |
findings-count |
Issues found |
deleted-count |
Caches deleted |
reclaimed-bytes |
Bytes reclaimed |
| Code | Meaning |
|---|---|
| 0 | Success, no issues |
| 1 | Issues found (useful for CI gates) |
| 2 | Invalid arguments |
| 3 | GitHub API error |
Cache Doctor reads cache metadata from the GitHub REST API (key, size, branch, last access time). It never reads cache content. It never stores your token. It has no backend, no database, no telemetry.
GitHub REST API cache-doctor Your workflow
│ │ │
│ GET /actions/caches │ │
│◄──────────────────────│ │
│ cache metadata │ │
│──────────────────────►│ │
│ │ analyze/lint/cleanup │
│ GET /branches/{name} │ │
│◄──────────────────────│ │
│ exists? 404? │ │
│──────────────────────►│ │
│ │ Job Summary / JSON │
│ │──────────────────────►│
Required permissions:
actions: readfor analyze and lintactions: writefor cleanup (needs delete permission)
| Feature | Free | Pro |
|---|---|---|
| Analyze (quota, stale, orphan detection) | Yes | Yes |
| Cleanup (delete stale/orphan caches) | Yes | Yes |
| Job Summary output | Yes | Yes |
| JSON output | Yes | Yes |
| Lint (19 static analysis rules) | Yes | |
| SARIF output (GitHub Code Scanning) | Yes | |
| PR comment output | Yes | |
| Hit/miss analysis (log parsing) | Yes |
Pro: $15/month or $119/year per organization. Get a license →
The free tier gives you full visibility into your cache health and automated cleanup. Pro adds preventive checks that catch bad patterns before they ship.
Pro licenses are per-organization and cover all repos in the org.
| Plan | Price | Link |
|---|---|---|
| Pro Monthly | $15/month | Subscribe → |
| Pro Annual | $119/year (2 months free) | Subscribe → |
| Commercial (AGPL exemption) | $149/year | Purchase → |
After payment, you'll receive a license key by email. It looks like this:
eyJlbWFpbCI6InlvdUBjb21wYW55LmNvbSIsInRpZXIiOiJwcm8iLC...
Go to Settings > Secrets and variables > Actions and create a secret:
- Name:
CACHE_DOCTOR_LICENSE_KEY - Value: the license key from step 2
For org-wide coverage, create it as an organization secret and grant access to all repos that use cache-doctor.
- uses: LeRedTeam/cache-doctor@v1
with:
mode: lint
env:
CACHE_DOCTOR_LICENSE_KEY: ${{ secrets.CACHE_DOCTOR_LICENSE_KEY }}Or via CLI:
export CACHE_DOCTOR_LICENSE_KEY="eyJlbWFpbCI6..."
cache-doctor lint .github/workflows/What happens when my license expires? There's a 7-day grace period. After that, Pro features (lint, SARIF, PR comments) stop working. Analyze and cleanup always work (free tier).
Can I use one license across multiple repos? Yes. Pro licenses are per-organization. One license covers all repos in the org.
Is there a free trial? The free tier includes full analyze and cleanup functionality. Try those first -- if you want lint rules to prevent future problems, upgrade to Pro.
AGPL-3.0. See COPYING.
Commercial licenses (AGPL exemption) available at the link above or contact leredteam@gmail.com