Dependency Caching Strategies for Faster CI
The biggest, cheapest CI speedup is not re-downloading the same dependencies on every run. Cache the package store, key it on the lockfile, and most installs become near-instant.
Dependency installation dominates many CI jobs. A good caching strategy restores the package store from a previous run so the installer only fetches what actually changed - turning minutes into seconds.
Cache the store, not the project
Prefer caching the package manager’s global store or download cache (npm cache, ~/.m2, pip wheel cache, Cargo registry) over a project directory like node_modules. The store is portable and lets the installer do a fast, validated install rather than blindly trusting copied files.
Key on the lockfile
The cache key should change exactly when dependencies change - a hash of the lockfile (package-lock.json, poetry.lock, Cargo.lock). Same lockfile → same cache → full hit. Changed lockfile → new cache entry, and a restore-key fallback recovers most of the previous store.
key: deps-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
deps-${{ runner.os }}-Common pitfalls
- Keying on a value that always changes → 0% hit rate.
- Caching
node_modulesdirectly, hiding lockfile drift. - Not including the OS/arch in the key → restoring incompatible binaries.
- Caching faster than you invalidate, serving stale transitive deps.
Measure the payoff
Compare install time on a cache hit vs a cold run; if they are similar, the cache is not hitting. A healthy dependency cache should turn a multi-minute install into seconds, and the hit rate should be high on branches that do not touch the lockfile.
Key takeaways
- Cache the package store/download cache, not the resolved project dir.
- Key on a lockfile hash; use restore-keys for near-miss fallback.
- Include OS/arch so you do not restore incompatible binaries.
- Verify by comparing hit vs cold install time - similar means it is not hitting.