Tuning
The defaults are already sensible. These are the knobs worth knowing about, in the order they matter.
The two that matter most for speed are FUSE options, so through mountx/auto they go in fuse: {…} and are ignored on a host that mounts over NFS. Importing mountx/fuse directly takes them at the top level.
#Caching — this is where the speed is
await mount(driver, "/mnt/point", {
fuse: {
attrTimeout: 10, // seconds the kernel may cache file attributes (default: 10)
entryTimeout: 10, // seconds it may cache name → file lookups (default: 10)
keepCache: true, // keep page cache between opens (default: true)
negativeTimeout: 0, // also cache "this file does not exist" (default: 0, off)
},
});These are worth far more than anything you can optimise in JavaScript. On the measured host, attrTimeout/entryTimeout are worth 8–15×, and keepCache ~4.9× on re-read. Nothing hand-optimised in the codebase comes close.
The trade-off is coherence, and it is entirely about who else writes to your storage:
- Nothing else writes it — raise the timeouts. The kernel answers
statfrom cache and your driver never hears about it. - Something else writes it — lower them, or leave them and call
notifyInvalInode()when you know. negativeTimeouthas the same caveat, sharper: a file created behind mountx's back stays invisible for the whole timeout. It is a real saving for a build that stats hundreds of missing headers, and off by default for exactly that reason (libfuse's default too).
Note
The traffic these settings absorb never reaches your driver at all. That is why the measured peak request rate is lower with the shipped defaults than with timeouts off — most of the work stopped happening.
#Concurrency
await mount(driver, "/mnt/point", { fuse: { readers: 2 } });readers is how many reads are kept outstanding on /dev/fuse at once, and it is really a threadpool knob. /dev/fuse is a character device, so libuv classifies it UV_FILE and every read parks one of the four default (UV_THREADPOOL_SIZE) pool threads until a request arrives — threads shared with all other fs, dns and zlib work in the process, including your driver's own I/O.
Two readers on the default pool leaves two threads for a node:fs-backed driver. Four would deadlock it.
To go higher, raise both — and UV_THREADPOOL_SIZE must be set before the process starts, because libuv reads it once:
UV_THREADPOOL_SIZE=32 node serve.ts # then readers: 8 is reasonableReplies do not use the pool at all — they are written synchronously, for exactly this reason — so readers plus whatever your driver needs is the whole budget.
#Write size
Raising the kernel's max_write from the 128 KiB default to 1 MiB was worth ~20% on sequential writes on the measured host — read that as "15–40%", and expect more from a driver doing real I/O. mountx already asks for 1 MiB by default; maxRead caps the other direction and defaults to the kernel's own.
#Other mount options
await mount(driver, "/mnt/point", {
// shared — the same meaning on both transports
readOnly: true, // the driver never sees writes
unmountTimeout: 10_000, // ms before unmount stops asking nicely
signals: true, // unmount on SIGINT/SIGTERM (default: true)
useDriverIno: true, // use the driver's own (dev, ino) identity (default: true)
fuse: {
fsname: "mydata", // what /proc/mounts shows as the device
subtype: "myfs", // makes the type read `fuse.myfs`
allowOther: false, // let other users in (default: false)
defaultPermissions: true, // let the kernel enforce mode bits (default: true)
},
nfs: {
exportPath: "/", // directory of the driver to export (default: "/")
nobrowse: true, // macOS only, on by default: keeps Finder and Spotlight out
hard: false, // retry forever instead of failing with EIO (default: false)
},
});Two of those deserve a sentence each:
defaultPermissions(FUSE, on by default) hands mode-bit enforcement to the kernel, so your driver never has to make access decisions. Turning it off means your driver is answering for every process on the machine.nobrowse(macOS, on by default) hides the volume from the GUI. A visible one is a volume Finder and Spotlight will crawl — for a JavaScript driver that means a burst of traffic and a scattering of.DS_Storewrites nobody asked for. Set itfalseif the point of the mount is for someone to open it in Finder.
#Telling the kernel something changed
If your storage changes behind mountx's back, drop the kernel's cached copy:
if (mounted.transport === "fuse") {
mounted.notifyInvalInode(42n); // forget this file's cached data and attributes
mounted.notifyInvalEntry(1n, "hello.txt"); // forget this name → file mapping
}Both take inode numbers as bigint, and both are FUSE-only — which is what the transport check narrows to. With mountx/fuse imported directly they are just there.
This is the release valve that makes long cache timeouts safe: keep attrTimeout high, and invalidate precisely when you know something moved.
#How fast is it, actually
All numbers here and above come from .agents/benchmarks.md and were taken on one host, on one day: Linux 6.12.96+deb13-amd64, 16 × Intel i7-10700K @ 3.80 GHz, Node v24.18.0, in-memory driver. They are not portable, and that file has the full tables and caveats.
- Throughput — sequential read 7,537 MiB/s served from page cache, 1,526 MiB/s with the cache off. Sequential write 796 MiB/s over FUSE, 308 MiB/s over NFS.
- Request rate — FUSE handles 41,500–50,300 requests/sec with requests in flight and timeouts off (1/2/4 readers: 41,539 / 48,549 / 50,349), and 13,600/sec for a strictly one-at-a-time client. With the shipped defaults the measured peak is around 20,468 requests/sec — lower because most of that traffic never reaches the driver.
- Syscalls — a single-threaded client sees 2,000–4,000/sec on uncached metadata, since one syscall is several FUSE requests.
#Next
- Troubleshooting — what to do when it wedges.
- FUSE and NFSv3 — per-transport detail.
- Reference — every option, in full.