How I made GitHub Actions builds 80% faster with caching

Our pipeline took ~12 minutes per push. The team was drifting away from CI/CD because the feedback loop was too slow. Most time went to npm install and docker build from scratch — every single time.
First we isolated dependency caching. With actions/cache and a key based on package-lock.json hash, npm ci dropped from ~3 minutes to ~20 seconds. Simple on paper, biggest impact in practice.
Then Docker. We used docker/build-push-action with cache-from/cache-to and the registry as cache backend. Base image layers and dependency installs stopped rebuilding every run.
Final result: ~12 minutes down to ~2.5 — roughly 80% faster. The team started pushing every commit again because they weren't waiting around.
Lesson learned late: cache keys should be based on lockfiles, not branches — or you get wrong cache hits and weird bugs. Once a stale feature branch hit main's cache and we nearly shipped wrong dependency versions.
Don't treat cache as speed-only — invalidation strategy matters too. We keep a manual "purge cache" workflow for when something smells off. Cheap insurance; it saved us once.
Another common mistake: caching node_modules without proper restore-keys. If the exact key misses and there's no fallback, you start from zero while the step still looks green.
Every pipeline now prints a short cache hit rate summary. Below 70% means something broke in key strategy or layer structure. Our pipeline motto: measure, don't guess.