Benchmarks
The question isn't a ratio, it's "if I run my agent in agentfs instead of straight on disk, how much slower is it?" So: real commands, a real toolchain, a real checkout, wall clock. Everything below is one machine — re-run before quoting.
target/ lives
inside the mount, which is where the bytes and the deep paths are.
Real workloads, through the sandbox
Each task runs inside agentfs run, so the
number includes namespace setup, the bind set, and the mount — not
just the filesystem in isolation. native is the same
task with no sandbox at all: the floor.
| Task | native | FUSE | kernel | FUSE × | kernel × |
|---|---|---|---|---|---|
| cargo build (clean) | 35.97 s | 38.63 s | 39.74 s | 1.07× | 1.10× |
| cargo build (incremental) | 0.824 s | 1.819 s | 0.839 s | 2.21× | 1.02× |
| cargo test | 9.43 s | 10.54 s | 8.67 s | 1.12× | 0.92× |
| git status (dirty) | 8.3 ms | 26.4 ms | 9.6 ms | 3.19× | 1.16× |
| git add + commit | 10.2 ms | 50.1 ms | 27.7 ms | 4.93× | 2.72× |
| grep -r | 11.6 ms | 77.1 ms | 13.4 ms | 6.67× | 1.16× |
| sandbox startup | — | 7.6 ms | 10.2 ms |
The inner loop is free on the kernel backend. The
incremental rebuild — the thing you feel dozens of times an hour —
runs at 1.02× the speed of no sandbox at all, and grep -r
collapses the same way, 6.67× down to 1.16×.
What's left is git add + commit at 2.72×. Git renames
every object into place, and a rename across layers is a copy-up;
that's probably intrinsic to layering rather than ours, but
"probably" isn't a measurement and nothing here separates the two.
In absolute terms it's 18 ms.
The FUSE column isn't dead weight — it's the fallback where the kernel path isn't available, and it's what runs on machines without unprivileged user namespaces.
What the checkpoint watcher costs
Automatic checkpoints are on by default, so their cost is real. Two separate taxes: probing for change, paid continuously, and snapshotting, paid only when a checkpoint fires.
The workload here is agent-shaped — 12 rounds of edit a source file, rebuild, commit — chosen because it moves the paths the probe actually watches rather than only build output, which the ignore list excludes.
| Watcher | FUSE | kernel |
|---|---|---|
| off | 22.99 s | 10.14 s |
| on (default) | 23.39 s | 10.19 s |
| on, forced every 5 s | 24.66 s (4 checkpoints) | 10.96 s (2 checkpoints) |
Probing costs about 1% and doesn't separate from noise. On the kernel backend — which has no FUSE server to keep a counter and so must stat-walk the layer — four alternating off/on pairs give 10.177 s against 10.293 s, with the ranges overlapping.
More convincing than the delta: it matches the model. A 10.2 s run at
a 500 ms probe interval is ~20 probes, and the walk measures 6.3 ms
on a corpus with a full .git. Predicted 126 ms, measured
116 ms.
A snapshot costs 0.32–0.39 s on this corpus,
effectively the same on both backends — the walk hashes contents and
freezes the process tree either way. At default settings a
continuously-busy agent produced zero checkpoints in every
run: idle needs a pause, and the ceiling is 90 seconds.
The cost arrives with the checkpoints, and checkpoints arrive at rest.
Where the FUSE time goes
For the fallback path, the overhead decomposes cleanly. A cold lookup costs 32.03 µs against native's 2.08 µs, and that splits:
| Component | Cost | Share |
|---|---|---|
| FUSE transport (kernel ↔ userspace) | 21.44 µs | 67% |
| our handler | 10.59 µs | 33% |
That two-thirds/one-third split held across three independent re-measurements. Two-thirds of the FUSE cost is the boundary itself and unreachable from our side — which is the argument for the kernel backend existing, rather than for optimising the handler further.
Things that sounded like wins and were refuted by measurement: resolution caching (the mechanism works 15× and moves no workload), writeback caching (43% fewer write ops, 0% time), and multi-threaded dispatch (+2.1 s at one thread and at four alike). The two changes that actually halved the overhead were a one-line dependency bump and deleting a memset.
What we don't claim
Three numbers came out flattering and are published here as artifacts rather than results, because they can't be true.
cargo test at 0.92×
The table above shows the kernel backend beating native. It can't:
it's a layer over the same ext4, with an extra layer in between.
native runs first with colder caches on a task with ~10%
run-to-run variance. Read it as parity.
Clean build at 1.07× / 1.10×
Both within noise of each other on a CPU-bound task — rustc
on 4 vCPUs is the bottleneck, not the filesystem. The kernel column
being the larger of the two is not meaningful at this sample size.
Sequential write at 1066 MB/s vs native's 235
An early fio run had agentfs 4.5× faster than the disk it writes to. Native sits at ~235 MB/s in every run ever taken on this machine, which is the disk's throughput — so the buffered writes were being dirty-throttled there and evidently not through the FUSE daemon. It measures page-cache absorption, not durable throughput, and is not a write benchmark. It's omitted rather than quoted.
Known gaps
- One machine. Treat sub-20% differences as noise. The clean build alone varies 17% run to run.
-
Rust and Python corpora only. A JS project —
npm install, 50k tiny files — would land considerably worse and isn't represented. - Copy-up isn't measured properly anywhere. It's the most frequent agent mutation and its cost is proportional to file size, so it needs its own test stratified by size. This is the biggest hole.
-
The probe tax is one corpus size. It follows
walk_cost / 500 ms, so a repo with a very large non-ignored file count pays proportionally more. - Nothing is gated in CI. Perf thresholds on shared runners flap and get muted; these are recorded for comparison, not enforced.
How it's measured
- Task timing is taken inside the sandbox, so the per-run startup constant doesn't land in the task's number. Startup is reported separately in the table above.
-
Each arm verifies which backend it actually got
before recording anything.
autofalling back silently would make the table compare FUSE against FUSE. - Backend order is reversed on a second pass. Whoever runs last benefits from warmed caches, and on the clean build that effect is larger than any real difference between arms.
- Incremental figures are the median of three, which agreed to within 4%.
Reproducing
$ cargo build --release $ ./bench/backends.sh # FUSE vs kernel, through `agentfs run` $ ./bench/checkpoint-tax.sh # what the watcher costs $ ./bench/real-workload.sh # the filesystems on their own $ ./bench/cost-model.sh # where the FUSE time goes
Each writes a TSV next to itself. BENCHMARKS.md in the
repo carries the full tables, the refuted hypotheses, and the
reasoning behind each measurement in more detail than belongs on a
web page.