CI Cache Audit: 46% of Dependency Installs in Top JS Repos Run Cacheless
I pulled the workflow directories of GitHub's 500 most-starred JavaScript/TypeScript repositories, mostly to see how other people write their cache keys.
After looking through them, nobody is writing keys wrong. Of the 422 repositories with workflows, 380 install dependencies, and 107 of those have no cache configuration at all.
There are 2,032 workflows that install dependencies. 1,098 have cache configuration. The remaining 934, 46%, never touch the cache.
First, the scanning methodology
The criteria were set before I started, not chosen after scanning:
Of the 500 repos, 78 have no workflow directory, leaving 422 repos with 4,845 files total. The fetch only pulls the workflow directory; not a single byte of business code is downloaded:
git clone --depth 1 --filter=blob:none --sparse <repo>
git sparse-checkout set .github/workflows
Detection is regex-based: npm ci, yarn, pnpm install, bun install count as installing dependencies; cache: npm (setup-* managed cache) or uses: actions/cache@ count as having cache.
Regex is not a YAML parser, so I can't see caching hidden inside composite actions or reusable workflows. The numbers below are lower bounds, not an audit report.
Cache keys: nobody is getting them wrong
This is the violation I set out to catch, and I caught none.
Of the 1,098 workflows with caching enabled, 818 use managed caching:
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
That one line encodes three rules: the key is built from runner.os, the Node version, and a hash of the lockfile; the path points to npm's global cache directory; and on a hit, the download is skipped. You don't need to remember the path, and you shouldn't change the key, because changing it turns you back into the hand-written crowd.
Among the hand-written actions/cache group, of 362 dependency cache steps, 317 include hashFiles(...lock...) in the key, 87.6%.
I initially marked all keys using commit IDs as incorrect. After spot-checking mastra and airi, I crossed them out: they have restore-keys after the key, so each run produces a new key and the fallback still hits the previous run's cache. This is standard build-cache practice, not an error.
The tutorial line "your key must include the lockfile hash" is already fully absorbed by the top repositories.
The real problem: no caching at all
For the 934 workflows without cache, I gave each one a reason:
131 explicitly disable caching with package-manager-cache: false; 46 repos have turbo or nx remote cache; 38 install dependencies inside docker build, relying on image layers.
After removing those three categories, 733 remain with no excuse. Some are lint-only pipelines that install dependencies just to run prettier; not caching those isn't unfair, but they don't need a separate workflow either.
At the repository level it's more direct: 107 repositories, with 279 dependency-installing workflows, have never enabled cache once.
From my experience, the hurdle isn't "how to write the key", it's "whether to touch CI at all." Many repositories' pipelines were set up by someone earlier; if it runs, nobody touches it, and nobody accounts for the three extra minutes.
Another kind of waste: caching that's equivalent to not caching
129 jobs cache node_modules, and 65 of them also run npm ci. Of those 65, 44 are written correctly:
- uses: actions/cache@v4
id: cache
with:
path: node_modules
key: deps-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npm ci
if: steps.cache.outputs.cache-hit != 'true'
On a hit, skip install; on a miss, install.
The other 21 (across 7 repositories) are missing that if line: after the cache is restored, npm ci runs unconditionally. And the first thing npm ci does is delete node_modules entirely and reinstall from scratch.
headlessui's install job is like this: it caches **/node_modules, and the next line is run: npm ci.
The cache download, extraction, and final save are all wasted. No error, no warning, just slow.
Three self-checks
# Is caching enabled?
grep -rn "actions/cache\|cache: npm\|cache: pnpm\|cache: yarn" .github/workflows
# Is caching configured but unused?
grep -rn -A 6 "uses: actions/cache" .github/workflows | grep -B 6 "npm ci"
The third check has to be done by hand: wherever node_modules is cached, npm ci must be preceded by a cache-hit check. If you don't want to add the check, don't cache node_modules; cache the package manager's own directory instead.
| What to cache | Path | Key |
|---|---|---|
| Package manager cache (recommended) | ~/.npm, ~/.cache/yarn, pnpm store |
One cache: line in setup-node |
| node_modules | node_modules |
lockfile hash, and a required cache-hit guard |
| Build artifacts | .next/cache, dist |
source hash, with restore-keys |
My position
Stop making "how to write the cache key" the first lesson. First check whether caching is enabled at all, then worry about writing it correctly.
Caching is not free either: the repository-level default is 10GB of quota (overages are allowed now, but they cost money), and entries are cleared after 7 days of non-use. Small projects with few dependencies may find restoring the cache slower than a direct install. Before enabling it, time one install; if it's under two seconds, don't bother.
Some will say
"High-star repos aren't representative of ordinary projects." True, which is why these numbers should only be read as a floor: if the top repos are like this, the levels below will have more.
"Self-hosted runners have local caches and don't need actions/cache." Fair point, regex can't see that, so I treat 733 as an upper bound.
"Installing dependencies multiple times per PR is the real waste." I tried to count by trigger, but the numbers didn't add up (307 of 380 repos reported zero PR installs), I don't trust that data, so I left it out.
Which category does your repository fall into: no cache at all, cache configured but never hit, or keys that are correct? Report your number in the comments.