Engineering Diaries · CI/CD
§
CI/CDBuild SystemsFrontend Infrastructure

Your CI Cache Is a Second Package Manager. Treat It Like One.

A CI cache can cut minutes from a build, or quietly make your release process non-reproducible. The difference is whether you treat it as disposable acceleration or hidden infrastructure.

01 · The Small Lie

A Cache Is Not Just a Faster Download

A pull request turns red in CI. Someone clicks rerun. It turns green without a code change. I have watched teams treat that as ordinary weather: the runner had a bad day, the network was slow, CI is flaky.

Often the real explanation is less random. The first run resolved one set of artefacts. The second run found a different cache. A dependency tarball, a build output directory, a browser binary, or an entire package-manager store changed the environment underneath the same commit.

That is why I no longer describe CI caching as a performance feature. It is a second package manager: a system that selects and reuses inputs your build depends on. Unlike npm, pnpm, or Yarn, it is usually undocumented, weakly versioned, and owned by nobody.

If the same commit can produce a different result because of a cache hit, the cache is part of your build contract.

The useful distinction is simple. A dependency lockfile should determine what enters a build. A cache should only make fetching those exact inputs cheaper. The moment the cache decides what gets used, it has become hidden infrastructure.

02 · What Actually Gets Cached

Not All Caches Have the Same Blast Radius

Teams often say they cache dependencies, but that phrase hides several very different things. Caching the package manager's download store is normally safe: the lockfile still decides the dependency graph, and the cache only avoids downloading already-known tarballs. Caching node_modules is riskier because lifecycle scripts, native modules, and the package-manager version all become part of the implied contract.

Caching build outputs is another category entirely. A compiler cache may be brilliant when its key includes source content, compiler version, configuration, platform, and relevant environment variables. It becomes a source of ghost bugs when the key is merely the branch name.

A practical rule

Cache immutable downloads aggressively. Cache derived outputs only when you can explain every input that makes them valid. Never cache a directory merely because it made one benchmark look good.

CacheUsually safe keyCommon failure
pnpm storeOS + lockfile + pnpm versionStore reused across incompatible Node versions
node_modulesAll of the above + install flagsStale native modules or skipped scripts
Build outputContent hash of every build inputConfig or environment omitted
Browser binariesPlaywright version + OSBinary and driver mismatch
03 · The Key

A Cache Key Is a Dependency Declaration in Disguise

The right question is not, what directory should we cache? It is, what would make the contents of this directory invalid? The answer becomes the cache key.

For a frontend build, that list is longer than people expect. The lockfile matters. So does the Node version, package-manager version, operating system, architecture, build-tool version, workspace configuration, and sometimes a value such as NEXT_PUBLIC_API_URL. If changing a value can change output, that value belongs in the cache identity.

GitHub Actions · dependency-store cache
key: ${{ runner.os }}-node-${{ steps.node.outputs.version }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
path: ~/.local/share/pnpm/store

This is not a universal key. It is an example of the thinking: encode the things that establish compatibility. A restore prefix can be useful for an immutable download store. It is much less defensible for a build artefact, where a near match may be worse than a miss.

Fast is not the first goalCorrect is the first goal. A cache miss costs time. A cache hit with invalid inputs costs time, confidence, and occasionally a release.
04 · Monorepo Trap

One Lockfile Does Not Mean One Useful Cache Boundary

Monorepos make this sharper. A single lockfile often means every package change invalidates one giant dependency-cache key. That is safe, but it can turn a small docs edit into a cold install for the entire repository. The predictable response is to weaken the key. That is where the trouble begins.

A better move is to separate concerns. Keep a broad, lockfile-keyed package-manager store cache. Then let your build system handle task-level reuse using explicit input hashes. Tools such as Turborepo, Nx, and Bazel exist partly because a repository is not a single build unit.

The important word is explicit. A task cache should know which source files, generated files, environment variables, and dependency outputs affect it. If the task has a secret input, do not quietly put the secret into a cache key. Instead ask whether that task should be cached at all, or whether its non-secret inputs can be separated from the secret-bearing step.

Do not use branch names as correctness keys

A branch is an ownership label, not a dependency graph. It can be a namespace for cache isolation, but it cannot prove an artefact is valid for a commit.

05 · False Greens

The Most Expensive Cache Bug Is a Green Build

A broken cache that causes a red build is annoying. A broken cache that causes a green build is more dangerous because it trains the team to trust an output that cannot be reproduced.

Frontend teams see this when generated type files are restored without the generator inputs, when a production build is reused after a configuration change, or when a test runner finds old transformed files. The deploy looks fine until a clean runner, a different region, or a customer's browser forces reality back into the process.

There is a simple diagnostic worth keeping: can the release artifact be rebuilt from a clean machine using only the commit, the lockfile, documented tool versions, and authorised secrets? If the answer is no, the cache has crossed the line from accelerator to source of truth.

0
Cache entries required for a correct clean build
1
Source of dependency truth: the lockfile
100%
Release artifacts should be traceable
∞
Times a cache may be safely discarded
06 · A Boring Design

Make Cache Misses Boring and Cache Hits Optional

The best CI cache is not the cleverest one. It is the one you can delete on a Friday afternoon without changing the output of the pipeline. Builds may slow down. They should not become different.

That leads to a deliberately boring design. Pin the runtime and package manager. Install from the lockfile using the package manager's frozen mode. Cache its download store. Build from source. Cache task outputs only with content-addressed keys. Publish an artifact that records the commit and build metadata. Test a clean path regularly.

  • Use pnpm install --frozen-lockfile, or the equivalent, in CI.
  • Put tool versions under version control rather than relying on runner defaults.
  • Treat a cache restore failure as a miss, not an exception to installation.
  • Schedule a clean build periodically, especially before changes to CI configuration.
  • Measure cache-hit rate and build duration, but also track flaky reruns.

This approach sounds less impressive than a heroic cache configuration. It is better engineering because the optimisation remains optional.

07 · Debugging

When CI Is Flaky, Inspect the Inputs Before the Retries

When a build passes on rerun, start by comparing the two environments. Which cache key was restored? Which Node and package-manager versions ran? Did the lockfile, generated assets, or environment differ? Was the runner image updated? Most CI systems put enough of this in logs, but we rarely log it in a form that makes comparison easy.

I prefer to print a compact build identity near the top of every important job: commit SHA, runner image, Node version, package-manager version, lockfile hash, cache key, and build mode. It turns a vague conversation about flakiness into a diff.

Build identity · shell
node --version
pnpm --version
shasum pnpm-lock.yaml
printenv | sort | grep '^NEXT_PUBLIC_'

Be cautious with environment logging: redact secrets and log names or non-sensitive hashes where needed. The goal is provenance, not an accidental credential dump.

08 · Verdict

Optimise the Build You Can Explain

CI caches are worth having. Slow feedback loops make developers batch changes, avoid tests, and lose concentration. A well-designed cache gives that time back.

But speed does not excuse mystery. The build system is part of your product's supply chain. If a cache key is too broad, you waste money. If it is too weak, you ship uncertainty. The sweet spot is not a magical configuration. It is a clear model of inputs, outputs, compatibility, and ownership.

So the next time someone proposes caching a large directory because it shaved three minutes from a job, ask one question before merging it: what exactly makes this directory valid? If the team can answer that in a sentence and encode it in the key, ship it. If not, keep the three minutes and the ability to sleep.

A cache should make a correct build faster. It should never be the reason a build is correct.
Written by Divyansh Singh. Views expressed are the author's own. Published August 2026. All code examples have been tested and verified.