Docker & Containers · Feb 2026 · 13 min read

BuildKit Cache Mounts: A Benchmark That Didn't Go the Way I Expected

I expected RUN --mount=type=cache to dramatically speed up repeated npm installs. I benchmarked it in a real GitHub Actions workflow instead of assuming, and the honest result was smaller than the hype — here's why, with the actual timing logs.

Starting From a Real Failure, Not a Clean Demo

The first attempt at this article used az acr build — the same tool the previous article in this series used to measure image sizes. It failed immediately:

Step 2/2 : RUN --mount=type=cache,target=/tmp/cache echo "buildkit cache mount works"
the --mount option requires BuildKit. Refer to https://docs.docker.com/go/buildkit/ to learn how to build images with BuildKit enabled

Azure Container Registry Tasks (az acr build) uses Docker's classic builder, not BuildKit, by default. This isn't documented prominently anywhere obvious, and it's a real gap in the previous article's own tooling choice — az acr build was the right call for measuring image sizes, but it can't run this benchmark at all. The fix was switching to a GitHub Actions runner, which has real Docker with docker buildx (BuildKit's CLI front-end) available via docker/setup-buildx-action.

Before Step 1, one term this walkthrough leans on:


Step 1 — Two Dockerfiles, one variable changed

# Dockerfile.no-cache-mount
FROM node:22-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
RUN npm run build
# Dockerfile.cache-mount
FROM node:22-alpine
WORKDIR /app
COPY package.json ./
RUN --mount=type=cache,target=/root/.npm npm install
COPY . .
RUN npm run build

Why only one line differs: isolating the RUN npm install line as the only change between the two Dockerfiles means any timing difference between them can only be attributed to the cache mount — not to a different base image, a different app, or a different dependency set.


Step 2 — The benchmark design, and why --no-cache is the actual test

- name: Warm the npm cache mount (first build, cold)
  run: time docker buildx build --no-cache -f Dockerfile.cache-mount -t cache-mount:1 --load .

- name: Rebuild with --no-cache (mount cache should still be warm)
  run: time docker buildx build --no-cache -f Dockerfile.cache-mount -t cache-mount:2 --load .

- name: Baseline build, no cache mount at all (first build, cold)
  run: time docker buildx build --no-cache -f Dockerfile.no-cache-mount -t no-cache-mount:1 --load .

- name: Rebuild with --no-cache (no mount cache to help this time)
  run: time docker buildx build --no-cache -f Dockerfile.no-cache-mount -t no-cache-mount:2 --load .

Why every build here passes --no-cache: this is the actual point of the benchmark, not an oversight. --no-cache forces BuildKit to discard ordinary layer caching entirely and re-run every instruction from scratch. If the second cache-mount build is still faster than the first despite --no-cache, that speedup can only be coming from the RUN --mount=type=cache directory — proving the cache mount is a genuinely separate mechanism from layer caching, not just another name for the same thing. The no-cache-mount builds are the control group: run twice, also with --no-cache, with nothing that should make the second one faster.


Step 3 — Running it for real and reading the actual timings

gh workflow run benchmark-cache-mount.yml --repo raphgm/docker-multistage-lab

The real results, pulled from the workflow's own logs:

BuildTotal time (real)
cache-mount, build 1 (cold)11.769s
cache-mount, build 2 (--no-cache, mount warm)9.255s
no-cache-mount, build 1 (cold)9.848s
no-cache-mount, build 2 (--no-cache)10.283s

At the total-build level, the pattern is there: the cache-mount build got faster on its second run (11.8s → 9.3s, about 21% less), while the no-cache-mount build didn't (9.8s → 10.3s — if anything, slightly slower, within normal noise). That's the expected direction.


Step 4 — The part that didn't match expectations

Looking at just the npm install step in isolation, inside BuildKit's own per-step timers, told a different story:

cache-mount build 1:      RUN --mount=... npm install  ->  3.337s
cache-mount build 2:      RUN --mount=... npm install  ->  3.118s
no-cache-mount build 1:   RUN npm install               ->  3.100s
no-cache-mount build 2:   RUN npm install               ->  3.409s

The npm install step itself took essentially the same ~3.1–3.4 seconds in all four builds, cache mount or not. The total-build-time difference observed in Step 3 is real, but it isn't coming from the npm install step being meaningfully faster — it's coming from noise elsewhere in the build (layer export, tarball transfer, buildx overhead), which varies run to run regardless of caching.

Here's the honest reason this benchmark doesn't show the dramatic effect the docs and blog posts promise: this app has 82 small dependencies, and for a dependency set that size, npm install's time is dominated by registry metadata resolution over the network — a fixed cost that happens whether or not the downloaded package tarballs are cached locally. A cache mount speeds up re-downloading packages; it does nothing for the metadata-resolution round-trip, which is the actual bottleneck at this scale.

RUN --mount=type=cache earns its keep on dependency trees large enough, or with slow/rate-limited enough registries, that skipping repeated downloads is the dominant cost — think hundreds of packages, private registries with authentication overhead, or ecosystems like Python's pip with large binary wheels. For a lean 82-package Node app hitting npm's CDN, the download itself was never the bottleneck, so there was nothing large for the cache mount to save.


Closing Thoughts

The instinct when a benchmark doesn't show what you expected is to either tweak it until it does, or quietly not publish it. Neither is honest. This cache mount benchmark is real — the workflow ran, the timings above are copied directly from GitHub Actions' own logs, not estimated — and the real result is "modest effect, and here's the specific reason why, tied to this app's actual dependency count." That's a more useful thing to know before reaching for --mount=type=cache on a small project than a headline number borrowed from someone else's much larger dependency tree.

GitHub Repository: docker-multistage-lab — includes Dockerfile.cache-mount, Dockerfile.no-cache-mount, and the exact benchmark-cache-mount.yml workflow used to produce every number in this article; re-run it yourself with gh workflow run.

Reviewed against current docker buildx and GitHub Actions behavior as of September 2026.

Docker · BuildKit · Cache Mounts · CI/CD · GitHub Actions