GitHub Actions · lesson 7 of 7
Beginner
2Intermediate
4Also part of GitHub Actions
5- + Bash Error Handling from Bash
- + CI/CD Pipeline Patterns: Sequential, Parallel, and Staged from DevOps
- + Terraform CI/CD with GitHub Actions (plan/apply) from Terraform
- + A CI Security Gate: Gitleaks, Bandit and pip-audit in One Workflow from DevSecOps (AppSec in the pipeline)
- + Migrating a Jenkins Pipeline to GitHub Actions from Jenkins
actions/cache: Measuring What a Cache Hit Actually Skips
A real cache miss and hit against the same workflow under act — the exact step a hit skips entirely, and the fixed act overhead a hit doesn't remove.
By SkillByExample · Published
actions/cache promises to skip expensive, repeatable work. Whether it actually does, and by
how much, is measurable — run the same workflow twice under act and compare.
The workflow under test
name: cache-test
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Restore cache
id: cache
uses: actions/cache@v4
with:
path: slow-output
key: slow-output-v3
- name: Slow step
if: steps.cache.outputs.cache-hit != 'true'
run: |
echo "cache miss - doing slow work"
mkdir -p slow-output
sleep 12
echo "done" > slow-output/result.txt
- name: Report
run: |
echo "cache-hit=${{ steps.cache.outputs.cache-hit }}"
cat slow-output/result.txt
sleep 12 stands in for whatever the real slow step would be — a dependency install, a
compile, a large download. The if on “Slow step” is what actually saves time; actions/cache
by itself only restores files and reports whether it found them.
First run: a real miss
$ act push -P ubuntu-latest=catthehacker/ubuntu:act-latest
⭐ Run Main Restore cache
| Cache not found for input keys: slow-output-v3
✅ Success - Main Restore cache [4.5543982s]
⭐ Run Main Slow step
| cache miss - doing slow work
✅ Success - Main Slow step [12.243717s]
⭐ Run Main Report
| cache-hit=
| done
⭐ Run Post Restore cache
| Cache saved successfully
| Cache saved with key: slow-output-v3
✅ Success - Post Restore cache [306.2339ms]
No cache existed for slow-output-v3, so “Slow step” ran for real — 12.24 seconds, matching
the sleep 12. cache-hit came back empty (falsy), which is what let the if condition let
“Slow step” run at all. At the very end, “Post Restore cache” saved the output for next time.
Second run, same key: a real hit
$ act push -P ubuntu-latest=catthehacker/ubuntu:act-latest
⭐ Run Main Restore cache
| Cache restored successfully
| Cache restored from key: slow-output-v3
✅ Success - Main Restore cache [4.7529941s]
⭐ Run Main Report
| cache-hit=true
| done
⭐ Run Post Restore cache
| Cache hit occurred on the primary key slow-output-v3, not saving cache.
✅ Success - Post Restore cache [232.3128ms]
“Slow step” doesn’t even appear in the log — its if condition evaluated false and it was
skipped entirely, not run-and-discarded. That’s the real 12.24 seconds this run didn’t spend.
cache-hit=true in “Report” confirms result.txt came from the restored cache, not a fresh run.
The number that isn’t a real saving
“Restore cache” itself took 4.55s on the miss and 4.75s on the hit — essentially the same,
despite one finding nothing and the other extracting a real cache archive. That’s because most
of this step’s time here is act’s own fixed cost: copying actions/cache’s bundled JavaScript
into the container and starting a node process, on every single run regardless of outcome.
This overhead is specific to running under act locally, not something a real GitHub-hosted
runner pays at the same scale — so the honest comparison this lesson can make is “12.24 seconds
of work, skipped entirely,” not “the whole job got roughly 4 seconds faster,” because the step
actions/cache itself runs in didn’t get faster at all.
Practice
1. Run the workflow a third time with the same slow-output-v3 key. Does "Restore cache" report a hit again, and does its timing change further?
⭐ Run Main Restore cache
| Cache restored successfully
| Cache restored from key: slow-output-v3
✅ Success - Main Restore cache [4.9418108s]
⭐ Run Main Report
| cache-hit=true
| done
Same hit, same skipped “Slow step”, and “Restore cache” stays in the same 4.5-4.9s band as both earlier runs — confirming that band is fixed per-run overhead, not something that trends down as the cache “warms up.” There’s no such thing as a warmer cache here; it’s either a hit or it isn’t.
2. Change the path in the cache step to a directory the "Slow step" doesn't actually write to (e.g. wrong-dir instead of slow-output), keep the same key, and run again. What happens?
⭐ Run Main Restore cache
| Cache not found for input keys: slow-output-v3
✅ Success - Main Restore cache [4.5212475s]
⭐ Run Main Slow step
| cache miss - doing slow work
✅ Success - Main Slow step [12.2197555s]
⭐ Run Main Report
| cache-hit=
| done
⭐ Run Post Restore cache
| [warning]Path Validation Error: Path(s) specified in the action for caching do(es) not
| exist, hence no cache is being saved.
✅ Success - Post Restore cache [224.7726ms]
Full miss, full 12-second “Slow step” again — a changed path invalidates the previously-saved
cache for that key, since actions/cache keys its lookup on the exact (key, path) pair, not
key alone. But it doesn’t stop there: “Slow step” still writes to slow-output (unchanged),
so wrong-dir never gets created — the save at the end genuinely can’t find anything at
wrong-dir to archive, and logs a real path-validation warning instead of silently saving an
empty cache. A mismatched path doesn’t just cost you a hit once; every subsequent run stays a
guaranteed miss until path and the step that actually populates it agree.
3. Add a second, unrelated fast step before "Slow step" that always runs (no if). Does it affect whether the cache hit still skips "Slow step" correctly?
⭐ Run Main Always runs
| this always executes
✅ Success - Main Always runs [267.154ms]
⭐ Run Main Restore cache
| Cache restored successfully
| Cache restored from key: slow-output-v3
✅ Success - Main Restore cache [6.5093383s]
⭐ Run Main Report
| cache-hit=true
| done
No — steps.cache.outputs.cache-hit is scoped to the cache step’s own id, not affected by
unrelated steps elsewhere in the job. “Slow step” still doesn’t appear in the log at all. (The
6.5s “Restore cache” timing here runs a bit above the 4.5-4.9s band the earlier runs
established — plausible container/host variance between individual act invocations, not a
change in what the step is doing.) The
if condition only ever depends on the specific step output it names.