THIS EXPLANATION
THE ROOM
CDA·22 Computing, Data & AI 6 MIN · 8 STATIONS

Build cache geometry

A Socratic walk-through of build cache geometry — reasoned out one step at a time, not lectured.

abcdefgh
a

The question we started with

THE QUESTION #

Why does swapping two lines in a build file change how long every future build takes?

Two build files. Same base image, same dependency install, same source copy, same final command — the same four instructions, and they produce byte-identical images. In one, the source is copied before the dependencies are installed; in the other, after.

The first rebuilds in four minutes every time somebody edits a source file. The second rebuilds in six seconds. Nothing was added or removed. So the interesting question is not "which order is faster" but what property of a cache can be so sensitive to order when the result is not sensitive to it at all.

b

Reasoning it through

REASONING #

Start with what the cache would have to know to reuse a step. Reusing an instruction's result is only safe if two things hold: the instruction itself is unchanged, and the state it would have been applied to is unchanged. The second condition is easy to forget, but it is the one doing all the work here. An instruction's output depends on what came before it, so a cache entry cannot be keyed on the instruction alone.

So the key is a pair: the identity of the parent state, plus the instruction. Which means cache lookups form a chain, not a set. Each step's key contains the previous step's key, so a miss anywhere invalidates everything downstream — not because the later instructions changed, but because the state they would run against is now different and therefore unproven.

Ask now what makes a step miss in the first place. For a command that merely runs something, the builder compares the command string; if it is identical it assumes the result is too. For an instruction that copies files in, it goes further and hashes the file contents, so an edit to any copied file changes the key. That is the honest and necessary behaviour — it is also exactly the mechanism that punishes bad ordering.

Now put the two together and follow your two files. When the source is copied first, every source edit changes that step's key. Everything after it is downstream, including the dependency install — so the install re-runs on every edit, even though package.json has not been touched in weeks. When the source is copied last, an edit changes only the final steps; the dependency layer's key is untouched, and it is reused.

Notice the shape of the rule that falls out. Order instructions so that the things that change rarely come before the things that change often. Not by cost, and not by logical grouping — by change frequency. Cost only decides how much the mistake hurts.

There is a corollary worth pulling out, because it is where most real build files go wrong even after the main lesson lands. To install dependencies before copying the source, you have to copy the manifest on its own first — the lock file and the dependency list, nothing else — then install, then copy the rest. That two-step copy looks redundant and is often "tidied up" into a single one by someone who has not thought about the chain. Splitting an input by how often its parts change is the whole technique.

A second corollary. Because the key includes the parent, a change to the base image invalidates everything, no matter how carefully the rest is ordered. Pinning the base to a stable reference is therefore part of cache design, not just of reproducibility.

One thing to be honest about: the newer BuildKit builder does not walk a strictly linear chain. It builds a graph of the build's stages and solves it, so independent stages can be cached and executed separately, and mount-based caches can persist a package manager's own directory across builds without living in any layer. That relaxes the constraint in useful ways — but within a single sequence of instructions the parent-key dependency still holds, so the ordering rule is unchanged. It is the surrounding freedom that grew, not the chain that went away.

c

The analogy

THE ANALOGY #
THE FIGURE

Think of a recipe written as a stack of prepared trays, where each tray can only be reused if every tray beneath it is exactly as it was. Put the ingredient you buy fresh every morning at the bottom, and every tray above it has to be remade daily, including the stock that takes four hours and never changes. Put the fresh ingredient at the top and the four-hour stock survives untouched from one day to the next.

The finished dish is the same either way. What differs is how much of the stack the morning's shopping invalidates.

WHERE IT BREAKS DOWN

the trays in a kitchen genuinely spoil, whereas nothing about the dependency install actually goes stale when a source file changes — the invalidation is a conservative assumption made by a cache that cannot inspect what an instruction really depended on, not a fact about the work.

d

Clarifying the model

THE MODEL #

Three refinements.

First, the misconception: cache misses are usually attributed to the step that is slow, so people try to make the install faster. The slow step is rarely at fault. It is being invalidated by something cheap and volatile sitting above it, and moving the cheap thing down costs nothing and fixes it entirely.

Second, this is path dependence in the proper sense: the cost of a build depends on the order in which equivalent operations were arranged, even though the outcome does not. That is why the effect is invisible in code review by anybody checking correctness — both files are correct, and only one is cheap.

Third, a copy instruction's key is content-based, so it is worth controlling what "the source" means. Copying the whole working directory sweeps in build output, editor files and version-control metadata, any of which can change the key without changing anything the build cares about. An ignore file is not housekeeping here; it is part of the cache key, and a missing one produces misses that look inexplicable.

e

A picture of it

THE PICTURE #
Build cache geometry
Build cache geometry The horizontal axis is how often an instruction's inputs change; the vertical is how long it takes to run. Read the chart from left to right as the order the instructions should appear in the build file. The top-left region holds the expensive, stable work that must sit early to be protected. The bottom-right holds the volatile, cheap work that belongs last, where invalidating it costs almost nothing. The top-right is the uncomfortable corner -- expensive and volatile -- and the label there says what to do about it: split the instruction so the stable part of its input moves left, which is exactly the manifest-then-source trick. {"generator":"[email protected]","source":"../Socrates/.diagram-cache/_src/build-cache-geometry.md","sourceIndex":1,"sourceLine":4,"sourceHash":"5eac035951fa4372a80986cf24f26108aaa8577ffa31134cd5c34315e680b9ce","diagramType":"quadrantChart","layoutVariant":"source","repairedDuplicateIds":[],"motion":"entrance-with-reduced-motion-fallback","presentation":"editorial","attempt":1,"viewBox":{"x":0,"y":0,"width":720,"height":621},"qa":{"passed":true,"findings":[]}} Split it Q1 Place early Q2 Safe anywhere Q3 Place last Q4 Metadata label Source copy Test suite Dependency install Base image Rarely changes Changes often Cheap step Expensive step Where an instruction belongs in the order

How to readThe horizontal axis is how often an instruction's inputs change; the vertical is how long it takes to run. Read the chart from left to right as the order the instructions should appear in the build file. The top-left region holds the expensive, stable work that must sit early to be protected. The bottom-right holds the volatile, cheap work that belongs last, where invalidating it costs almost nothing. The top-right is the uncomfortable corner — expensive and volatile — and the label there says what to do about it: split the instruction so the stable part of its input moves left, which is exactly the manifest-then-source trick.

f

What became clearer

WHAT CLEARED #
WHAT CLEARED

A build cache is a chain in which every entry's key contains its parent's, so a miss propagates forward and never backward. That single property turns instruction order into a performance decision independent of correctness: sort by how often each input changes, put the volatile things last, and split any instruction whose input mixes the two.

g

Where to go next

ONWARD #
  • What BuildKit's graph solver changes, and what it leaves exactly as it was.
  • Cache mounts for package manager directories, and why they do not become layers.
  • Sharing a cache between a laptop and a CI runner, and why CI so often misses everything.
h

Key terms

TERMS #
TermWhat it means
Cache keythe identifier a builder computes for a step, combining the parent state's identifier with the instruction and, for copies, a hash of the file contents.
Invalidationthe discarding of a cached result because its key no longer matches, which cascades to every downstream step.
Path dependencethe property that an outcome's cost depends on the order operations were performed in, not only on the operations themselves.
BuildKitthe current build engine, which resolves a build as a dependency graph and supports caches held outside the image layers.

Every term the collection defines is gathered in the glossary.

Nearby on the shelf

4