Numbers from script/fs-bench.sh and a full
npm install of cloudflare/sandbox-sdk
(854 packages, 36,675 files), running
examples/container-legacy on a Cloudflare
Containers standard-2 instance (1 vCPU, 6 GiB memory, 12 GB disk).
The computerd FUSE mount lives at /workspace; the comparison columns are
an in-memory tmpfs at /tmp and the container's ext4 root disk at
/var/tmp.
Ratios are computerd / baseline — lower is faster, values below 1.0 mean
computerd beats the baseline.
| Scenario | computerd | tmpfs | tmpfs ratio | ext4 disk | disk ratio |
|---|---|---|---|---|---|
| tiny-file churn | |||||
| create 1000 files | 560.6 ms | 83.2 ms | 6.7x | 303.2 ms | 1.85x |
| stat 1000 files | 1971.9 ms | 1324.2 ms | 1.49x | 2659.3 ms | 0.91x |
| rm 1000 files | 827.7 ms | 322.7 ms | 2.56x | 1281.8 ms | 0.66x |
| directory traversal | |||||
| mkdir tree (10×10×10) | 1597.5 ms | 1585.7 ms | 1.01x | 3034.7 ms | 0.74x |
| find tree | 1813.6 ms | 1819.9 ms | 1.00x | 4404.2 ms | 0.72x |
| large file I/O | |||||
| write 64 MiB | 230.6 ms | 47.3 ms | 4.87x | 16.8 ms | 16.93x |
| copy 64 MiB | 1037.2 ms | 37.4 ms | 27.75x | 39.8 ms | 40.46x |
| read 64 MiB | 437.5 ms | 22.6 ms | 19.33x | 25.6 ms | 39.72x |
| pure read 64 MiB | 263.1 ms | 8.3 ms | 31.54x | 8.5 ms | 30.26x |
| pure copy 64 MiB | 852.9 ms | 21.7 ms | 39.27x | 22.0 ms | 41.47x |
| overwrite 64 MiB | 272.6 ms | 8.3 ms | 32.91x | 8.5 ms | 43.35x |
| git | |||||
| git init + commit 100 files | 459.2 ms | 40.3 ms | 9.56x | 635.4 ms | 0.72x |
| git clone (shallow, ~1MB) | 549.1 ms | 421.0 ms | 1.30x | 576.2 ms | 0.84x |
| npm | |||||
| npm init + tiny install | 598.5 ms | 630.7 ms | 0.95x | 630.7 ms | 0.95x |
| Target | Duration |
|---|---|
tmpfs (/tmp) |
34.3 s |
computerd FUSE (/workspace) |
124.7 s |
ext4 disk (/var/tmp) |
63.9 s |
computerd is ~2x slower than the container's ext4 disk for the full
npm install, and ~3.6x slower than tmpfs. The disk comparison is
the more realistic baseline for general usage.
Important
These numbers measure the mount, not the pull. They stop when
npm install returns. What follows — moving 36,675 files into the
Durable Object — is not counted here, and for a dependency tree it is
the larger cost.
#179 reports an
install timing out at 120 s and then taking ~3 further minutes to
return while the partial node_modules was pulled, after which the
next command failed with a storage timeout the workspace did not
recover from. None of that is visible in the table above.
If you are sizing a workload against these figures, add the transfer
yourself, or keep the tree out of sync entirely — see
computerd: Local-only paths.
A path matched by MOUNT_IGNORE is served from the container's disk
rather than the VFS.
There are two savings, and they are independent.
The first is in what is skipped. An ignored write does not enter the VFS, the SQLite store, the change-pack encoding, or the pull into the Durable Object. For a dependency install, that is the difference between transferring tens of thousands of files and transferring none, and it holds on every host.
The second is FUSE passthrough. On Linux 6.9 or newer, opening a
local-only file hands the file to the kernel, which then serves its
reads, writes, and mmap straight from the container's disk. The bytes
never reach computerd. What still reaches it is metadata: lookups,
stat, directory listings, and each open and close.
So passthrough brings data-heavy work close to raw disk, and leaves metadata-heavy work where it was:
fs-bench scenario |
synced path | local-only, passthrough off | local-only | raw disk |
|---|---|---|---|---|
| write 64 MiB | 50.5 ms | 49.1 ms | 21.4 ms | 16.8 ms |
| overwrite 64 MiB | 66.8 ms | 51.8 ms | 22.2 ms | 18.9 ms |
| read 64 MiB | 89.7 ms | 74.4 ms | 22.3 ms | 19.5 ms |
| pure read 64 MiB | 49.8 ms | 34.0 ms | 6.0 ms | 6.4 ms |
| copy 64 MiB | 202.9 ms | 172.4 ms | 67.5 ms | 32.5 ms |
| create 1000 files | 421.1 ms | 314.3 ms | 245.9 ms | 20.6 ms |
| stat 1000 files | 876.2 ms | 695.0 ms | 712.6 ms | 397.6 ms |
A dependency install sits between the two, because it creates many small
files. These are medians of npm install runs, followed by reading every
installed file back once:
| Install | synced path | local-only, passthrough off | local-only | raw disk |
|---|---|---|---|---|
express (604 files), install |
599 ms | 473 ms | 508 ms | 385 ms |
express, read back |
193 ms | 114 ms | 118 ms | 6 ms |
| front-end toolchain (5,412 files, 88 MB), install | 3,676 ms | 2,279 ms | 2,104 ms | 1,329 ms |
| front-end toolchain, read back | 2,307 ms | 957 ms | 700 ms | 48 ms |
The toolchain is TypeScript, Vite, webpack, ESLint, React, and
@types/node. On an install as small as express, the gap between the
two local-only columns is within run-to-run noise. Expect a local-only
install to land between raw disk and the synced mount, nearer the disk
the larger the files are, and expect the larger saving to be the
transfer that no longer follows it.
Note
These numbers come from an arm64 Linux 7.0 virtual machine on a
laptop, with Docker's overlayfs as the backing store, not from
Cloudflare Containers hardware. Treat the ratios as the result and the
absolute times as local. script/run-fs-bench.sh (with LOCAL_ONLY=1)
and script/run-npm-bench.sh (with IGNORE_NODE_MODULES=1) reproduce
them, with COMPUTERD_FUSE_NATIVE_ROOTS=0: local-only directories are
eligible for native mounts by default where supported (next section).
Passthrough leaves metadata on computerd's event loop. Where supported,
computerd can bind-mount eligible local-only directories from the
container's disk, so nothing under a mounted root reaches computerd.
See
computerd: Native roots.
These runs were all taken on the same machine, one after another. The raw-disk column is the median of the baselines measured alongside each run, and the local-only columns have passthrough on.
| fs-bench scenario (median, ms) | synced | local-only, passthrough | local-only, native root | raw disk |
|---|---|---|---|---|
| create 1000 files | 441.4 | 317.6 | 13.7 | 14.7 |
| stat 1000 files | 851.1 | 674.5 | 347.9 | 363.2 |
| rm 1000 files | 600.9 | 320.2 | 20.9 | 22.7 |
| mkdir tree (10x10x10) | 682.7 | 607.1 | 303.6 | 310.7 |
| find tree | 674.5 | 554.2 | 304.5 | 305.6 |
| write 64 MiB | 50.7 | 18.4 | 15.4 | 16.9 |
| copy 64 MiB | 214.6 | 64.9 | 31.6 | 33.2 |
| read 64 MiB | 96.6 | 19.6 | 24.0 | 22.7 |
| pure read 64 MiB | 53.6 | 6.0 | 8.1 | 6.1 |
| pure copy 64 MiB | 151.9 | 41.9 | 14.9 | 14.1 |
| overwrite 64 MiB | 67.3 | 17.1 | 16.8 | 16.2 |
| git init + commit 100 files | 192.5 | 108.4 | 12.5 | 12.4 |
| git clone (shallow, ~1MB) | 710.8 | 561.7 | 668.2 | 558.3 |
| npm init + tiny install | 184.3 | 178.3 | 180.5 | 181.8 |
| Install (median, ms) | synced path | local-only, passthrough | local-only, native root | raw disk |
|---|---|---|---|---|
express (589 files), install |
550 | 470 | 392 | 379 |
express (589 files), read back |
176 | 73 | 6 | 6 |
| front-end toolchain (5,428 files, 91 MB), install | 3,643 | 2,165 | 1,365 | 1,336 |
| front-end toolchain (5,428 files, 91 MB), read back | 2,114 | 707 | 52 | 50 |
Metadata-heavy local work under a native root approaches raw disk, where
passthrough alone leaves directory operations on computerd's event loop.
git clone is dominated by the network and varies between runs.
Note
Same caveat as above: an arm64 Linux 7.0 virtual machine on a laptop
(OrbStack), not Cloudflare Containers. Reproduce with
script/run-fs-bench.sh (LOCAL_ONLY=1) and script/run-npm-bench.sh
(IGNORE_NODE_MODULES=1); add COMPUTERD_FUSE_NATIVE_ROOTS=0 for the
passthrough column. These numbers predate the descriptor-confined mount
helper; they measure steady-state file operations, not mount setup time.
Writeback caching would cut metadata round trips further on writes. It stays off: libfuse 3 can negotiate it, but it changes write behavior for the synced mount as well, which is a separate decision.
The Cloudflare Containers figure is still open:
| Scenario | Install duration | Bytes pulled into the DO |
|---|---|---|
npm install to a synced path |
124.7 s | (not yet measured) |
npm install to a MOUNT_IGNORE path |
(not yet measured) | 0 by construction |
Note
The empty cells are deliberate. They are for the same
cloudflare/sandbox-sdk install used above, on the same instance
type, and that has not been run. The zero in the last cell is a
property of the design — an ignored path produces no sync entries —
not an observation.
computerd keeps its SQLite store in memory by default. Set
COMPUTERD_DB to a path and it goes on the container's disk instead.
These numbers come from script/store-compare.mjs, which uses the
dofs filesystem directly with 2,000 files in one directory. There is
no FUSE mount involved, so any difference is down to the store.
| Operation | memory | on disk (64 MiB cache) | ratio |
|---|---|---|---|
| create 2000 files | 180.9 ms | 659.1 ms | 3.64x |
| stat 2000 paths, cold | 1580.4 ms | 1558.7 ms | 0.99x |
| stat 2000 paths, warm | 12.1 ms | 15.1 ms | 1.25x |
| readdir x50 | 155.4 ms | 143.1 ms | 0.92x |
Reads are not slower on disk. That holds even when the cache is far too small for the tree: a 2 MiB cache against a 3.8 MiB database still reads at 0.99x. Two reasons. Most of the time goes on walking the path rather than fetching pages, and the operating system caches whatever SQLite drops.
Writes are slower, and the reason is the cost of flushing to disk.
Creating 1,000 files takes 445 ms at synchronous = full, 292 ms at
normal (what we ship), and 148 ms at off — which matches the
in-memory store's 181 ms.
Through a real FUSE mount the write cost shrinks, because the mount itself is already the bigger expense:
| Scenario | memory store | file store | baseline |
|---|---|---|---|
| stat 1000 files | 2777.3 ms (1.10x) | 3114.9 ms (1.22x) | ~2540 ms |
| create 1000 files | 989.4 ms (0.98x) | 1178.7 ms (1.17x) | ~1010 ms |
| write 64 MiB | 238.1 ms (11.14x) | 221.9 ms (12.69x) | ~19 ms |
| overwrite 64 MiB | 294.3 ms (26.16x) | 304.6 ms (29.30x) | ~11 ms |
This is what the on-disk store buys. script/restore-time.mjs times
what a host waits after a restart: connect, compare sync positions,
and send whatever the other side is missing.
| Tree | memory store | file store |
|---|---|---|
| 500 files | 480 ms (502 entries sent) | 26 ms (0 sent) |
| 3,000 files | 3749 ms (3002 entries sent) | 23 ms (0 sent) |
An in-memory store sends the whole workspace again after every restart, so its cost grows with the tree. A file store sends nothing, because the sync positions came back along with the files. Restoring takes about 25 ms whatever the size, so the saving grows too: 18x at 500 files, 161x at 3,000.
The trade: small-file work costs 10 to 20 percent more, and a restart costs a flat 25 ms instead of resending everything.
Two things these numbers do not cover. They come from one Linux
container, not from Cloudflare Containers hardware, and a full
cloudflare/sandbox-sdk npm install has not been run. The restore
figures also drive the sync protocol in process, so they show the work
avoided but not the network round trips a real host would also skip.
The in-memory inode store beats real disk on metadata-heavy work:
stat, rm, mkdir tree, find tree, git init, git clone,
npm init. Those eight scenarios cover most of the day-to-day cost
of tools like git status, module resolution, and incremental
builds.
Large sequential file I/O. The computerd write path hashes each
CHUNK_SIZE (512 KiB) chunk
into a content-addressed blob store on every release; that's how
the Durable Object can sync only the chunks that changed and
deduplicate identical content. The cost lands on raw
dd-style throughput numbers but rarely on real developer
workloads, which is why npm init + tiny install matches the disk
baseline despite pure read 64 MiB being 30x slower.
bash script/run-fs-bench.shor against a deployed computerd-container instance, upload
script/fs-bench.sh and run it with
MOUNT=/workspace BASE=/tmp.