# Basic caching

> Basic caching in CI refers to storing dependency packages, compiled artifacts, and Docker layers between pipeline runs so they don't need to be re-downloaded or re-built from scratch on every commit.

Perspective: delivery
Source: https://visdom-maturity-matrix.virtuslab.com/guides/delivery/basic-caching

## What It Is

Basic caching in CI refers to storing dependency packages, compiled artifacts, and Docker layers between pipeline runs so they don't need to be re-downloaded or re-built from scratch on every commit. It is the first and most accessible optimization available to teams trying to speed up their CI pipelines, and it's available natively in every major CI platform - GitHub Actions, CircleCI, BuildKite, and GitLab CI all provide built-in cache primitives that require no additional infrastructure.

The mechanism is straightforward: before a CI job runs its main steps, it checks a cache store (keyed on a hash of your dependency lock file) for previously computed outputs. If the cache hits, it restores those outputs and skips the download or build step entirely. If the cache misses (because the lock file changed), it runs the step normally and saves the output to the cache for the next run. The result is that 80-90% of CI runs - those that don't change dependencies - skip the most time-consuming setup steps entirely.

For dependency managers, caching typically saves 3-8 minutes per run. npm, pip, maven, and gradle all benefit significantly. A Node.js project that installs 500 packages in 4 minutes without caching consistently installs in 15-30 seconds with a warm cache. For Docker builds, layer caching means that layers whose inputs haven't changed are served from cache rather than rebuilt - a 5-minute Docker image build becomes a 30-second layer diff push. These are among the highest-ROI changes available in CI optimization: meaningful speedup with minimal engineering effort.

For teams adopting AI agents, basic caching is the prerequisite that makes further optimization possible. Agents generate more CI runs per hour than human developers do. A team that goes from 5 pushes per day to 50 pushes per day (driven by agent activity) without caching will see its CI runner costs and queue times multiply by 10x. Caching absorbs most of that load increase because dependency downloads, the most expensive step, are served from cache rather than network.

## Why It Matters

- **Most impactful CI optimization per hour of engineering effort** - dependency caching typically saves 3-8 minutes per run and can be implemented in 1-2 hours with native CI platform primitives
- **Scales CI cost linearly with dependency changes, not with commit frequency** - agents generate many more CI runs than humans; caching ensures those runs don't multiply infrastructure costs proportionally
- **Required foundation for all further optimization** - incremental builds, test impact analysis, and distributed caching all assume a functioning basic cache layer; caching must come first
- **Reduces runner capacity requirements** - faster runs mean runners are available sooner for the next job in the queue; caching reduces the required runner pool size for a given throughput
- **Improves developer experience immediately** - 5-minute runs that drop to 2 minutes after caching is enabled are visible and tangible; the team sees the improvement without waiting for a longer optimization project

## Getting Started

1. **Cache your package manager's dependency directory** - For GitHub Actions with npm: use `actions/cache` with `node_modules` and key on `package-lock.json` hash. For Gradle: cache `~/.gradle/caches` and key on `*.gradle` and `gradle-wrapper.properties`. For pip: cache `~/.cache/pip` and key on `requirements.txt`. Each CI platform has documented examples for the most common package managers.
2. **Use restore-then-save patterns correctly** - The cache key must reflect the actual inputs. Use a hash of your exact lock file (not your manifest file) as the primary key. Add a fallback key that matches the most recent cache for the same branch. This ensures cache hits on unchanged dependencies and cache restoration even when the lock file has minor updates.
3. **Enable Docker layer caching** - If your pipeline builds Docker images, add layer caching with `docker/build-push-action` and `cache-from: type=gha` (GitHub Actions cache) or `cache-from: type=registry` (external registry). Ensure your Dockerfile ordering puts the most stable layers (base image, system packages) before the most volatile layers (application code) so cache hits are maximized.
4. **Cache compiled build artifacts** - For compiled languages (Java, Go, Rust), cache the output of the compilation step. Gradle's build cache, Go's GOCACHE, and Rust's target directory all support caching between CI runs. Key these caches on the source files that affect compilation (not just dependencies).
5. **Verify cache is actually hitting** - After enabling caching, check your CI logs to confirm cache hits are occurring. GitHub Actions shows "Cache hit" or "Cache miss" in the cache step log. A run after a no-lock-file-change commit should show "Cache hit." If it's consistently missing, check your cache key construction.
6. **Set a cache size limit and cleanup strategy** - Large caches (multi-gigabyte node_modules, large Maven repositories) can exceed platform limits or become expensive to store. GitHub Actions has a 10GB limit per repository. Monitor cache size and add `.cacheexclude` patterns for artifacts that don't benefit from caching (logs, temporary files, test reports).

> **Tip**: Cache key design is where most teams get caching wrong. If you cache on a key that's too broad (e.g., just the branch name), you'll get stale cache hits. Too narrow (e.g., the full lock file content plus a timestamp), and you'll never hit cache. The standard pattern - primary key as lock file hash, restore key as branch-prefix - balances freshness with hit rate.

## Common Pitfalls

**Caching node_modules instead of the package manager cache directory.** Caching `node_modules` directly is brittle: it can contain platform-specific binaries that don't transfer across runner operating systems, and it bypasses the package manager's integrity checks. Cache `~/.npm` (npm's download cache) instead and let `npm ci` reinstall from the local cache. This is faster than downloading from npm's registry and safer than restoring `node_modules` directly.

**Not invalidating stale caches when workflows change.** If your CI pipeline changes in ways that affect how dependencies are installed (new CI steps, different install flags), old cached artifacts may be incompatible. Add a manual cache version suffix to your cache keys (`v1-{hash}`) that you can increment to force a full cache rebuild when the pipeline changes significantly.

**Caching test outputs alongside build outputs.** Test result files and coverage reports should not be cached between runs - they're run-specific artifacts. If they get included in your cache and restored in future runs, subsequent runs may report false results (showing old test output rather than the current run's output). Be explicit about what's in your cached directories.

**Forgetting to cache in the right job.** If your pipeline splits into multiple jobs (lint in one job, test in another), each job needs its own cache configuration. Caching only in the "test" job doesn't help the "lint" job, which may be downloading the same dependencies independently. Configure caching in every job that installs dependencies.

**Assuming caching solves the problem.** Basic caching is a necessary first step, not a complete solution. A pipeline that takes 18 minutes will typically drop to 10-12 minutes with caching, which is meaningful progress but not the 5-minute target. After caching is in place, the next step is parallelization and incremental builds. Don't stop at caching.

## Bob - Head of Engineering

Bob's team has 18-minute CI and he's just committed to a 30-day target of 10 minutes. He's not sure where to start, and the team has limited bandwidth for CI work. He wants the highest-ROI first move.

Bob should direct the CI owner to implement dependency caching in the first two days. It's a configuration change, not a refactor - a few lines of YAML in the CI config - and typically produces 5-8 minutes of speedup immediately. Bob should ask for a before/after timing report after caching is enabled so he can quantify the improvement and decide on next steps. If caching drops CI from 18 to 11 minutes, the team is close to the 10-minute target and can likely get there with a small amount of test parallelization. If caching drops CI from 18 to 14 minutes, there's a bigger structural problem (maybe the test suite itself is slow) that needs investigation. The caching data tells Bob which problem he actually has.

## Sarah - Productivity Lead

Sarah wants to show the team that CI optimization is worthwhile, but she's been struggling to make the case with anecdotes. "CI is slow" is a complaint, not a business case. She needs data that shows the cost of current slowness and the benefit of optimization.

Sarah should calculate the "dependency download tax" - how many developer-hours per week are spent waiting for dependency downloads across all CI runs. If the team runs 200 CI jobs per day and each currently downloads dependencies for 4 minutes, that's 800 developer-minutes per day (assuming one developer is waiting per run). After caching, that drops to 800 minutes × (30 seconds / 240 seconds) = 100 developer-minutes per day. The savings is 700 developer-minutes - nearly 12 developer-hours - per day, from a configuration change that takes 2 hours to implement. That's the ROI calculation that makes CI optimization a no-brainer. Sarah should run these numbers, present them to Bob, and use them to justify prioritizing CI work in the next sprint.

## Victor - Staff Engineer - AI Champion

Victor has already set up basic caching for his own repositories and knows it works. He's noticed that different teams on the same GitHub organization are each maintaining their own cache configurations - some correctly, some incorrectly (wrong keys, wrong directories). The result is inconsistent CI performance across teams even though the fix is simple and standardized.

Victor should write a shared workflow template that encapsulates the correct caching configuration for each supported tech stack (Node.js, Python, Java, Go). Teams can call this template with `uses: org/ci-templates/.github/workflows/cache-setup.yml@main` rather than copying YAML into each repository. The template centralizes the correct implementation and ensures consistency. Victor should also add a "caching health check" job to the template that logs whether the cache hit or missed on each run, making it easy to audit whether caching is actually working across the organization.

## Links

- [GitHub Actions - Caching dependencies](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows)
- [CircleCI - Caching dependencies](https://circleci.com/docs/caching/)
- [GitLab CI - Cache dependencies and results](https://docs.gitlab.com/ee/ci/caching/)
- [BuildKite - Artifacts and caching](https://buildkite.com/docs/pipelines/artifacts)
- [Docker - Optimize layer caching](https://docs.docker.com/build/cache/)
