CI Build Caching
Caching cuts build time dramatically.
Layers
CI build caching is the difference between pipelines that take 4 minutes and pipelines that take 40. Most teams run pipelines without aggressive caching; the result is repeated work on every run that compounds across hundreds of runs per day. The caching strategies are well-understood; applying them captures 50 to 80% time savings on most builds.
What Docker layer caching achieves:
- Docker layer cache.: Each instruction in a Dockerfile produces a cached layer. Subsequent builds reuse the cache up to the first changed instruction; everything before the change reuses the cache. The cache is the largest single source of CI speedup for container-based workloads.
- Saves 80% of build time.: The base image, OS packages, language runtime, dependency installation are typically the largest layers. Each is invariant across most builds; the cache reuses them. The build that took 8 minutes without caching takes 2 minutes with caching.
- First-job slow; subsequent fast.: The very first build populates the cache. After that, every build hits cache for the unchanged layers. The first build takes the full time; subsequent builds take a small fraction. The amortization is favorable across many runs.
- Cache key matters.: The cache key is computed from the layer's content. Identical inputs produce cache hits; changed inputs produce cache misses. Ordering Dockerfile instructions to maximize cache hits (stable layers first, frequently-changing layers last) is the optimization.
- BuildKit cache mounts.: Modern Docker (BuildKit) supports cache mounts for tools like apt, pip, npm. The mount persists across builds without becoming part of the image. The cache speeds up dependency operations; the image stays clean.
Docker layer caching is the single largest CI optimization for container-based pipelines. Enabling it correctly is the highest-leverage configuration change most teams can make.
Dependencies
The second category of caching is dependency caching outside the container layer. Language-specific dependency managers (npm, pip, Maven, Gradle, Cargo, Go modules) have their own caches; CI pipelines should integrate with them.
- Lock file hash as cache key.: The dependency manager's lock file (package-lock.json, requirements.txt, go.sum) determines which dependencies will be installed. Hashing the lock file produces a cache key that is invariant across runs that have the same dependencies.
- Restore on miss.: If the cache key matches a previous run, restore the dependency directory from cache. The install step finds the dependencies already there and is essentially a no-op. The minutes saved per run accumulate quickly.
- Each language has its own pattern.: npm cache lives in ~/.npm. pip cache lives in ~/.cache/pip. Maven cache lives in ~/.m2. Each has its own conventions; CI providers have native support for caching the standard locations.
- Per-OS caching.: Different operating systems may have different binary dependencies (compiled extensions, platform-specific binaries). Cache keys should include the OS to prevent serving wrong binaries from cache.
- Cache size limits matter.: Most CI providers have caps on cache size and total cache storage. Large caches get evicted; cache hit rates drop. Monitoring cache hit rate and adjusting size or eviction policies is part of the maintenance.
Dependency caching is the second pillar of CI speed. After Docker layer caching, dependency caching captures the next-largest segment of avoidable repeated work.
Test results
The third caching layer is test result caching: skipping tests whose inputs have not changed. Tools like Bazel, Nx, and Turborepo support this natively; teams that adopt the tooling capture significant additional speedup.
- Cache test outputs for unchanged code.: A test that ran successfully on a specific commit does not need to run again on a subsequent run that has the same inputs. The cache stores the test result; the run skips the actual execution and uses the cached result.
- Skip tests; run only diff.: The CI run only executes the tests whose inputs changed. The unchanged tests get cached results; the changed tests run fresh. The execution time scales with the change size, not the suite size.
- Bazel, Nx, Turborepo support.: Each computes a content-addressable hash of the test's inputs (source files, dependency versions, environment). Identical hashes produce cache hits; new hashes produce execution. The discipline is correct hash computation; tools handle it.
- Distributed cache for the team.: The test cache can be shared across the team. A test that ran successfully on engineer A's PR does not need to run again on engineer B's PR if the inputs are identical. The team-wide cache hit rate compounds the per-engineer benefit.
- Validation that cache is correct.: Test result caching is correctness-sensitive. A bad cache (stale cached result for code that has changed) produces false-pass results. The hash computation must be precise; invalidation must be aggressive when in doubt.
Test result caching is the third pillar of CI speed. Combined with Docker layer caching and dependency caching, the cumulative speedup transforms multi-minute pipelines into sub-minute ones. Nova AI Ops watches CI duration and cache hit rates as first-class metrics, surfaces the cases where caching is underperforming, and helps the team see the optimization investments compounding over time.