Six Isolated Agents Cost 24 Kilobytes
Container isolation for a fleet of agents costs 48 seconds once and 4.1 kB per agent after — until the bind mount that makes it convenient costs 465 MB each and hands the sharing back.
Six containers, each with its own fully isolated 465 MB dependency tree, added 24.6 kB of disk between them.
That is not a typo, and it is the number that makes the whole argument. In the previous post I measured what git worktree gives a fleet of coding agents: 44 tracked files isolated in 132 ms, and 23,199 gitignored ones left out in the cold. The escape hatch every ADE offers for that gap — symlink node_modules from the primary checkout — is not isolation at all, and its own source says so.
That post ended by pointing at the container and never priced it. This one does.
First, a correction. That post’s last figure claimed a container image build is “three orders of magnitude slower” than a worktree. Measured, it is 364× — 48.1 seconds against 132 milliseconds. Between two and three orders, not three. The caption has been fixed; the number I should have had before writing it is below.
What the up-front bill actually is
The workspace image is the obvious one: a Node base,1 the lockfile, npm ci, then the source on top.
FROM node:24-bookworm-slim
WORKDIR /w
# Dependency layer. Rebuilt only when the lockfile changes.
COPY package.json package-lock.json ./
RUN npm ci
# Source layer. Rebuilt on every edit; cheap because it is 44 files.
COPY . .
Ordering COPY before RUN npm ci would be the same image and a completely different cost curve, which is the entire point of what follows.2 Here is where the 48.1 seconds goes, with the base image already pulled:3
npm ci is 28.0 s of it and the image export is another 19.3 s. Copying the 44 source files takes 0.1 s.
The consequence is the second half of that figure. Edit a source file and the rebuild is 1,124 ms, because the dependency layer is a cache hit. Touch package-lock.json and it is 48,172 ms — the whole thing again. The cost is not “building an image”; it is “invalidating the dependency layer”, and an agent doing ordinary work does not invalidate it.
That asymmetry is the product. A fleet of agents editing source pays about a second each; a fleet of agents upgrading dependencies pays 48.
What each additional agent costs
Once the image exists, an agent workspace is a docker run:
$ for i in 1 2 3 4 5 6; do docker run -d --name agent$i agentws:1 sleep 600; done
Show data table
| docker run to running container | |
|---|---|
| agent 1 | 672 |
| agent 2 | 359 |
| agent 3 | 388 |
| agent 4 | 436 |
| agent 5 | 383 |
| agent 6 | 397 |
docker run -d to return, for six containers started back to back from the same image. The first pays a cold penalty; the rest settle into a 359–436 ms band. Total for the fleet: 2,635 ms.Against the worktree’s 132 ms, a warm container start is about 3×, not 364×. The 364× is a one-time cost that six agents amortise to 8 seconds each and sixty agents amortise to under a second.
Dispatching work into a container that is already up costs less still — docker exec into a running container measured 251 ms.
Then the disk:
$ docker ps -a --format '{{.Names}}\t{{.Size}}'
agent6 4.1kB
agent5 4.1kB
agent4 4.1kB
agent3 4.1kB
agent2 4.1kB
agent1 19.2MB # this one actually ran `npm run build`
Each container’s writable layer starts at 4.1 kB and grows only by what the task actually writes. agent1 ran the site build and is 19.2 MB — the dist/ it produced. The 465 MB of node_modules underneath all six is one read-only set of layers, shared by copy-on-write, counted once.
That is the structural answer to the problem the previous post ended on. An ADE that symlinks node_modules into every worktree gets one copy on disk and loses isolation, because a symlink is writable from every side.4 An image layer gets one copy on disk and keeps isolation, because the layer is read-only and each container’s writes land in its own overlay. Same disk, opposite guarantee.
Scale that to the fleet and the three strategies separate by five orders of magnitude. Six worktrees that each run their own install is 405.9 MB apiece — 2,436 MB. Six containers is 24.6 kB. And the arrangement most people actually reach for, which the next section measures, is 465 MB apiece — 2,790 MB, worse than the worktrees it was supposed to improve on.
Only the middle figure is a measurement rather than a multiplication, and it is the one that sounds made up.
Does the box slow the work down?
This is the question that usually kills the idea, so it is worth measuring rather than assuming. The same npm run build, three ways:
| Where | Time |
|---|---|
| Host, directly | 5,309 ms |
| Container, dependencies from the image layer | 4,712 ms |
| Container, host repo bind-mounted in | 11,546 ms |
The isolated container is faster than the host — about 11% — and the bind-mounted one is 2.45× slower than the isolated one.
The first result is less surprising than it looks: the container runs on a Linux filesystem in the VM, the host build runs on NTFS, and a build that touches twenty thousand small files is dominated by filesystem calls. On a Linux host the gap would close. The point is not that containers are fast; it is that isolation is not what costs you the runtime.
The mount undoes both wins
The third row is where this goes wrong, and it goes wrong in a way that will look familiar.
Nobody wants a 48-second rebuild in their edit loop, so the standard move is to bind-mount the working tree into the container and overlay node_modules with a volume so the host’s platform binaries do not leak in:
$ docker run -v "D:/workspace/TechBlog:/w" \n -v /w/node_modules -w /w agentws:1 npm run build
That one line costs three things. The build goes from 4,712 ms to 11,546 ms. The anonymous volume Docker creates to hold node_modules — seeded from the image path it covers5 — measures 465 MB and 22,939 files — per container, which is worse than giving every agent its own worktree install and 113,000× the 4.1 kB it replaced.
And the third cost is the one the previous post was about:
$ docker run -d --name m1 -v "D:/workspace/TechBlog:/w" ... agentws:1 sleep 300
$ docker run -d --name m2 -v "D:/workspace/TechBlog:/w" ... agentws:1 sleep 300
$ docker exec m1 sh -c 'echo "written by m1" > /w/dist/agent-marker.txt'
$ docker exec m2 cat /w/dist/agent-marker.txt
written by m1
$ cat dist/agent-marker.txt # on the host
written by m1
flowchart LR M1["container m1<br/>own namespaces, own layer"] -- "write /w/dist/x" --> H["host directory<br/>D:/workspace/TechBlog"] H -- "read /w/dist/x" --> M2["container m2<br/>own namespaces, own layer"] H -- "read dist/x" --> HOST["the host itself"] M1 -. "isolated: PIDs, network,<br/>installed packages, writable layer" .-> M2
Two containers. Separate PID spaces, separate network namespaces, separate writable layers — and one shared directory, which is the only directory either agent cares about. This is the symlinked node_modules from the previous post wearing a different hat: the isolation boundary is real everywhere except across the thing being worked on.
The failure mode is the same too. Two agents told to “fix the failing test” will both write dist/, both run the formatter across the tree, and neither one’s git status will explain what the other did.
What this buys, and where it stops
The honest summary is that a container is not a faster worktree or a slower one. It changes the shape of the cost.
Three things this does not solve, which matter before anyone treats it as the answer.
The image is a snapshot, and snapshots go stale. Every agent in the fleet is working against the dependency tree that existed when the image was built. That is a feature while the work is source-only and a trap the moment it is not — the agent’s node_modules and the repository’s lockfile can disagree, silently, exactly as they could with a shared symlink. The difference is that here the disagreement is pinned to a tag you can print, rather than to whatever the primary checkout happened to contain.
Per-agent disk is 4.1 kB only while agents write little. The one container that ran a real build was 19.2 MB. Six agents each producing a full dist/ is ~115 MB, not 25 kB. The chart’s bottom line is the floor, not the steady state.
Ports and external services are still shared. A container gets its own network namespace, which is a genuine improvement over six worktrees fighting over port 3000. It does not give each agent its own Postgres, its own S3 bucket, or its own third-party API quota. The previous post’s ladder still has a rung above this one.
And one caveat about the numbers themselves: docker images reports this image at 1.28 GB while docker image inspect reports 363.4 MB for the same image — those two are not measuring the same thing, and this host runs the containerd snapshotter.6 Neither figure is the one that matters for a fleet. The one that matters is what each additional container adds, and that is measured directly above.
Takeaways
- The container’s cost is up-front, not per-agent. 48.1 s once, then ~390 ms and 4.1 kB per agent. At six agents the setup amortises to 8 s each; the previous post’s “three orders of magnitude” was wrong, and the measured figure is 364×.
- Layer cache invalidation is the real cost driver. A source edit rebuilds in 1,124 ms; a lockfile change rebuilds in 48,172 ms. Put
COPY package*.jsonandRUN npm ciaboveCOPY . .or the ratio inverts. - An image layer is the shared
node_modulesdone correctly. One copy on disk, read-only, with each container’s writes in its own overlay — the same disk saving as a symlink, with the opposite guarantee. - Isolation is not what costs you runtime. The isolated container built this site in 4,712 ms against the host’s 5,309 ms.
- The bind mount costs all three wins at once. 2.45× slower builds, 465 MB per agent instead of 4.1 kB, and one shared directory that every agent can write — which is the exact problem containers were brought in to solve.
- Price the floor, not the marketing. Every number above came from
docker build,docker psandduon one ordinary repository, and took about ten minutes to collect. Do it on yours before choosing a layer.
References
Footnotes
-
node:24-bookworm-slim— the base image every measurement here is built on. ↩ -
Docker documentation, Cache invalidation and Optimize cache usage — why instruction order determines which edits invalidate the dependency layer. ↩
-
Measurements were taken on 2026-09-17 against this blog’s own repository: Docker 28.0.4, Docker Desktop on Windows 11, overlayfs with the containerd snapshotter, 8 CPUs and 16.5 GB available to the engine. Times are wall clock around the CLI invocation, so they include client overhead; disk figures are
docker ps --size,docker history, andduinside the container. The base image pull is excluded throughout — it failed twice against the registry CDN before succeeding, so any figure for it would describe the network rather than the tool. ↩ -
Orca documentation, Worktrees — the
worktree.sharedDirectoriesmechanism the image layer is being compared against, covered in detail in the previous post. ↩ -
Docker documentation, Bind mounts and Volumes — the two mount types used above, and the rule that mounting a volume over a populated image path seeds it from that path. ↩
-
Docker documentation, containerd image store — the image store in use on the host these measurements were taken on. ↩