How Do I Measure Cache Hit Rate in GitHub Actions?
Read the cache-hit output of the restore step on every run, write it to the job summary, and price the count against the cache line in the billing report.
Read the cache-hit output of the restore step in each run and count how often it comes back true, which takes one extra step that writes the outcome into the job summary and one gh loop over recent runs. Then pair that count with the cache line in the billing report, because a hit rate only means something against what the cache costs and how many minutes a hit actually saves.
Answer
The measurement has a numerator and a denominator that live in different places. The numerator comes from the workflow, where the cache action exposes a cache-hit output that is true only when an entry matched the primary key exactly, and false on a restore-keys prefix match or a miss (WarpBuild caching documentation). The denominator is every run of that job.
Nothing records that boolean for you, so add a step that does. This version writes a machine-readable marker to both the step log and the job summary:
jobs:
unit-tests:
runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v4
- name: Restore dependency cache
id: deps
uses: WarpBuilds/cache/restore@v1
with:
path: |
~/.npm
node_modules
key: npm-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-${{ runner.arch }}-
- name: Record cache outcome
if: always()
run: |
if [ "${{ steps.deps.outputs.cache-hit }}" = "true" ]; then
result=hit
else
result=miss
fi
echo "CACHE_RESULT job=unit-tests entry=deps result=$result label=${{ runner.os }}-${{ runner.arch }}" \
| tee -a "$GITHUB_STEP_SUMMARY"
- name: Install dependencies
if: steps.deps.outputs.cache-hit != 'true'
run: npm ciThe marker goes to stdout as well as the summary, which is what makes it countable later. Logs are what the GitHub CLI can read back, so the rate over the last 50 runs of a workflow is one command:
gh run list --workflow ci.yml --branch main --limit 50 \
--json databaseId --jq '.[].databaseId' |
while read -r id; do
gh run view "$id" --log | grep -o 'CACHE_RESULT .*result=[a-z]*' || true
done |
grep -o 'result=[a-z]*' | sort | uniq -cThe output is two lines, result=hit and result=miss, with counts. Divide and you have the exact-match rate for that job on that branch over that window. Run logs are retained for 90 days by default and the period is configurable per organization (GitHub log retention settings, checked on 2026-08-13), so retention bounds the window rather than the workflow. Count per branch: the default branch and pull request branches read different entries, and a blended number hides the case where main is warm and every pull request is cold.
Two properties of the number are worth stating before anyone reports it. A prefix restore through restore-keys counts as a miss here, because cache-hit is false for it, so what you are measuring is the rate at which the install step gets skipped entirely. And a job that never writes an entry has a hit rate of zero forever, which reads as a cache problem when it is a save-step problem.
Detail
The cost side of the ratio
The Cache tab of the Reports page carries the other half. It shows a daily chart broken down by cache type, summary cards for total cost, storage cost, operations cost, and total entries, a per-entry table, and CSV export of every row matching the current filters. The cache type filter offers storage, operation-hit, and operation-commit.
| Line in the Cache tab | What it bills | Hosted rate | BYOC |
|---|---|---|---|
storage | Gigabytes held per month | $0.20 per GB-month | Free |
operation-hit | Reads and list calls | $0.0001 per operation | Free |
operation-commit | Writes | $0.0001 per operation | Free |
Rates from the WarpBuild caching documentation, checked on 2026-08-13. Those rows count billed operations rather than exact-key matches, so they price the cache and do not replace the cache-hit count as the numerator.
The Jobs report supplies the third input. It aggregates per repository, workflow, and job name, with run count, success rate, and duration P75 and P90 (Reports documentation), which is where the warm and cold step durations come from without stopwatch work. Reading utilization alongside them is covered in GitHub Actions observability and runner metrics.
The rate at which a cache stops paying for itself
A cache earns its place when the minutes it saves on hits outweigh the minutes it adds on misses plus the storage it holds. Write it as a break-even:
break-even rate = (monthly fixed cost / runs + cost per miss) / (saving per hit + cost per miss)
Take a job on warp-ubuntu-latest-x64-4x at $0.008 per minute (pricing page, checked on 2026-08-13), 1,000 runs a month, a 2 GB entry, an install that takes 4 minutes cold and 30 seconds warm, a restore that takes 24 seconds, and a save that takes 30 seconds. Substitute your own durations from the Jobs report; the rates are fixed.
- Saving per hit: 4.0 minus 0.5 warm minus 0.4 restore = 3.1 minutes, or $0.0248.
- Cost per miss: 0.1 minute of lookup plus 0.5 minute of save = 0.6 minutes, or $0.0048, plus one write at $0.0001, so $0.0049.
- Fixed per month: 2 GB at $0.20 = $0.40 of storage, plus 1,000 restore operations at $0.0001 = $0.10, so $0.50.
- Break-even: (0.0005 + 0.0049) / (0.0248 + 0.0049) = 0.0054 / 0.0297 = 0.18.
Below roughly an 18 percent exact-match rate, that cache costs more than deleting the steps. The threshold moves a long way with the shape of the entry:
| Entry shape | Cold step | Warm step | Restore | Save | Size | Break-even rate |
|---|---|---|---|---|---|---|
npm downloads and node_modules | 4.0 min | 0.5 min | 0.4 min | 0.5 min | 2 GB | 18 percent |
| Wide build output tree | 1.5 min | 0.5 min | 0.8 min | 0.9 min | 6 GB | 97 percent |
The second row is the one to look for. A large entry that saves 12 seconds of a 90 second step needs a rate almost nothing achieves, and the honest move is to narrow the path list until the restore is cheaper than the work it replaces, or to drop that entry and keep the cheap one. Cache hit rate covers the definition and the counting conventions behind these numbers.
Measure per label rather than per repository
The cache version hash covers the compression tool and the path list, so an entry written on warp-macos-latest-arm64-6x never restores on warp-ubuntu-latest-x64-4x (caching documentation). A matrix job that shares one key expression across labels therefore reports one blended rate that averages several separate caches, one of which may be at zero. The marker line above carries runner.os and runner.arch for exactly that reason. WarpBuild caching is not supported on WarpBuild Windows runners, where workflows keep actions/cache@v4 (caching documentation).
When the counted rate is near zero on every branch, the cause is usually the key rather than the store, and why a GitHub Actions cache misses every run works through the three scopes that decide a lookup. When the rate is healthy and the job is still slow, the restore itself is the cost, and the guide to persistent caches on GitHub Actions covers the mechanisms that keep state on the machine instead of moving it each run.
Related Questions
Does a restore-keys prefix match count toward the hit rate?
No. The cache-hit output is true only on an exact match against the primary key, so a prefix restore through restore-keys puts files on disk and still reports false (caching documentation). A rate counted from cache-hit is an exact-match rate, which is the number you want when the question is whether the install step gets skipped. Log the restore step duration next to the outcome if you also want to see partial restores, and read cache hit rate for how the two counts differ.
Can I read the hit rate out of the WarpBuild reports instead of the workflow?
The Cache tab of the Reports page gives you the cost side: storage cost, operations cost, total entries, and a daily chart by cache type, with CSV export. Those rows count billed operations rather than exact-key matches, so keep the cache-hit count from the workflow as the numerator and use the report for what the cache costs and for the job durations you compare it against. GitHub Actions observability and runner metrics covers the Jobs and Queue Timings sections alongside it.
What hit rate is high enough to keep a cache?
It depends on the two durations and the entry size, so compute the break-even instead of guessing. A 2 GB npm cache that turns a 4 minute install into 30 seconds on warp-ubuntu-latest-x64-4x breaks even near an 18 percent exact-match rate at $0.008 per minute (pricing page, checked on 2026-08-13), while a 6 GB entry that saves 12 seconds of a 90 second step needs about 97 percent and should be narrowed or dropped. When the answer is that no key can carry the rate, persistent caches on GitHub Actions is the next mechanism to try.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.