CI/CD·7 min read·

Caching in CI: Fast Pipelines Without Cheating

Cache dependencies and build outputs with correct keys so every run is fast and never silently stale.

NB

Netbay Cloud Team

Netbay Engineering

On this page

A pipeline that reinstalls and recompiles everything from scratch on every push is correct but painfully slow. Caching fixes that by reusing expensive-to-produce outputs across runs. The art is in the cache key: too coarse and you serve stale results; too fine and you never hit the cache. This post covers what to cache, how to key it, and how to keep caching correct.

What Should You Cache

Caching makes sense for inputs that change rarely relative to how often the pipeline runs. The classic targets:

  • **Dependency directories** — npm, pip, maven, go module caches.
  • **Lockfiles and frozen versions** — these are the cache keys, not the cache themselves.
  • **Build outputs** that are expensive to regenerate and stable.
  • **Toolchain downloads** — language runtimes, compilers.

You should generally *not* cache test results, coverage that depends on the source, or anything derived directly from code you change often. Caching those hides correctness problems.

The Cache Key Is Everything

Every cache entry has a key that determines whether it is a hit or a miss. The best keys include the *fingerprint of the thing that determines the cache content*. For dependencies, that fingerprint is the lockfile checksum.

yaml
      - uses: actions/cache@v4
        with:
          path: node_modules
          key: node-${{ hashFiles('package-lock.json') }}
          restore-keys: |
            node-

Read that carefully. To write the key without a literal ${{, GitHub Actions evaluates it at runtime — you write it as ${{ hashFiles(...) }} in your editor, but the expression resolves to a hash of the lockfile. If the lockfile changes, the key changes, the old cache is ignored, and a fresh one is created. If it does not change, you get a hit and the install is nearly free.

restore-keys is a fallback: when the exact key misses, Actions restores the most recent cache whose key starts with the prefix. That is a great safety net because even a slightly stale dependency cache is usually far cheaper to top up with one changed package than to rebuild cold.

Store Cache, Not Unit Tests

A common mistake is caching build outputs that your tests depend on, then running tests against the cached copy. If the cache is fresh, fine; if it is stale, you are testing a build nobody shipped. Cache *inputs* to make producing outputs fast, but derive the actual artifact fresh in the build stage so what you test equals what you deploy.

Example: Node Dependencies

Here is a full job using a lockfile-keyed cache:

yaml
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ hashFiles('package-lock.json') }}
          restore-keys: |
            npm-
      - run: npm ci
      - run: npm test

Caching ~/.npm (instead of node_modules) is often the smarter choice: npm ci still creates a correct node_modules from scratch, but the package tarballs come from a warm cache, so the install is much faster while remaining deterministic. Caching node_modules directly is brittle because you might reuse a directory with stale transitive bits.

lockfile unchanged same key cache HIT fast install lockfile changed new key cache MISS fresh install two paths either way correct

Cache Correctness Rules

A cache is only safe if it could never serve something your fresh build would not produce. The rules that keep you honest:

  • Always key on a digest of the dependencies, never on just the branch name.
  • Never let a cache cross a major toolchain change silently; include the tool version in the key.
  • Add a fallback restore, but always run the authoritative install so correctness wins.
  • Periodically expire caches; a stale cache is worse than no cache.

If you are unsure whether a cache helped or hid a problem, disable it once and compare the result. A well-configured cache should only change timing, never outcomes.

Self-Hosted Warm Caches

On a self-hosted runner the OS-level package cache stays warm automatically between runs. Documents and dependency caches persist on disk, so reinstalls are faster than on ephemeral shared runners. That is one of the quiet advantages of owning the box: your cache is persistent rather than evicted per-project.

Takeaway

Cache inputs, not outputs; key on a lockfile digest; restore as a fallback but always install authoritatively. Applied consistently, caching turns a multi-minute install into seconds while preserving correctness. Never let the cache be the thing that decides whether a build is valid.

For a persistent, warm-cache runner that keeps your builds fast, a Netbay VPS with SSD storage is a good fit — spin one up at netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance