Cut CI build times with dependency caches, matrix tuning, and Docker layer reuse in GitHub Actions. Practical YAML, pitfalls, and fixes. Read now.
Where CI Time Actually Goes
Most slow pipelines are slow for boring reasons: they reinstall the same dependencies on every run, rebuild Docker images from scratch, and run jobs one after another that could run side by side. Before you tune anything, look at the timing breakdown of a representative build and find the two or three steps that dominate. Optimizing a 20-second step is wasted effort when a dependency install or image build is eating the bulk of the wall-clock time.
The three highest-leverage moves in GitHub Actions are caching dependencies between runs, spreading independent work across a build matrix, and reusing Docker layers instead of rebuilding them. Each targets a different kind of repeated work, and they compose well.
Caching Dependencies
Dependency caches store the artifacts your package manager downloads so later runs restore them instead of fetching everything again. The actions/cache action, and the built-in caching in setup actions like actions/setup-node, both key the cache on a hash of your lockfile so it invalidates automatically when dependencies change. Cache the resolved package directory rather than the whole workspace, and keep the key tied to the lockfile plus the runner OS.
Watch for the two classic failure modes: a cache key so broad it serves stale dependencies, or so narrow it never hits. A good pattern is an exact key plus a looser restore-keys prefix, so a changed lockfile still restores a mostly-warm cache and only downloads the delta.
Tuning the Matrix
A build matrix runs the same job across several configurations in parallel. Used well, it turns a long serial sweep of versions or platforms into concurrent jobs that finish in roughly the time of the slowest one. Used carelessly, it multiplies your slowest job across combinations you don't actually need to test on every commit.
- Trim combinations with
excludeso you only run the pairings that matter. - Use
fail-fastdeliberately — leave it on for quick feedback, turn it off when you need results from every cell. - Set
max-parallelif unbounded fan-out starves your runner pool or blows past concurrency limits.
Keep the matrix focused on genuinely independent work. Splitting a test suite into parallel shards often pays off more than adding more platform variants that rarely break.
Reusing Docker Layers
Docker builds are cacheable at the layer level, but only if the layers stay stable. Order your Dockerfile so the least-frequently-changed steps come first: copy your dependency manifest and install dependencies before copying application source. That way a code change invalidates only the final layers, and the expensive dependency layer is reused.
In Actions, use Buildx with a cache backend so layers persist across runs rather than living only on an ephemeral runner. The common pitfall is invalidating the whole chain early — copying the entire working tree before installing dependencies means every commit busts the cache and rebuilds everything. Fix the layer order first, then wire up the cache export and import.