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.

Machine. Intel Xeon @ 3.10 GHz, 4 vCPU, 15 GB RAM, GCP persistent disk with ext4, kernel 6.17. Corpus is a checkout of agentfs itself — a Rust workspace whose 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.

TasknativeFUSEkernelFUSE ×kernel ×
cargo build (clean)35.97 s38.63 s39.74 s1.07×1.10×
cargo build (incremental)0.824 s1.819 s0.839 s2.21×1.02×
cargo test9.43 s10.54 s8.67 s1.12×0.92×
git status (dirty)8.3 ms26.4 ms9.6 ms3.19×1.16×
git add + commit10.2 ms50.1 ms27.7 ms4.93×2.72×
grep -r11.6 ms77.1 ms13.4 ms6.67×1.16×
sandbox startup7.6 ms10.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.

WatcherFUSEkernel
off22.99 s10.14 s
on (default)23.39 s10.19 s
on, forced every 5 s24.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:

ComponentCostShare
FUSE transport (kernel ↔ userspace)21.44 µs67%
our handler10.59 µs33%

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.

Why this section exists. Every one of these was caught by asking "is that physically possible?" of a number that favoured us. A benchmark page without one of these sections has either been very lucky or hasn't looked.

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. auto falling 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.