Server is the source of truth.
Local disk is a hot cache.
CNFS is a cloud-storage filesystem, not a local disk with a network cache bolted on. You can mount a 10 TB share with a 20 MB cache and drop a 100 MB file into it — the file streams to the server, the cache holds whatever fits, and reads of large files stream back without ever fully landing on your disk.
`df` shows the server, not your cache disk
A cnfs mount's logical capacity is the server volume's total / used / free, not the local cache directory. df /mnt/cnfs/<name> calls the server's statfs endpoint and reports the NAS' actual capacity. The local cache budget (set via CNFS_CACHE_SIZE_MB) is intentionally invisible at the filesystem level — it's a performance knob, not a capacity limit.
$ df -h /mnt/cnfs/photos Filesystem Size Used Avail Use% Mounted on cnfs-photos 8.0T 3.2T 4.8T 40% /mnt/cnfs/photos
Writes stream through, not buffer
When you copy a file into a cnfs mount, the kernel's write(2) calls go through a small in-memory ring buffer (default 16 MiB) and a goroutine drains the buffer into an in-flight HTTP PUT body. The kernel back-pressures naturally when the buffer fills, so the writing process blocks instead of seeing ENOSPC.
The server commits an upload by atomically renaming a staging path (<path>.staging-<uuid>) to the final destination once the body finishes. A mid-upload crash on either side never leaves a half-written file at the destination — the staging copy is GC'd.
Whether the file also lands in the local cache is decided by file size at open(2) time:
- • Fits in cache budget → the bytes streaming to the server are tee'd into a cache-staging tempfile, promoted to the cache slot when the upload commits.
- • Exceeds cache budget → bypass-cache mode. No local copy is kept. Future reads stream from the server.
Reads serve cache hits locally, stream cache misses
On open(2) for read, the client checks its file-level LRU at $HOME/.cache/cnfs/<mount>/. A hit reads at local-disk speed. A miss issues a streaming GET to the server; the bytes are fed to the kernel as they arrive and tee'd into the cache (if the file fits the budget).
Files larger than the entire cache budget skip the tee — every read serves from a streaming HTTP body, with random access driven by HTTP Range requests. Watching a 100 GB video on a 10 GB cache does not blow up the cache; it streams.
Cache eviction evicts cold uploaded files first
The local cache is a sliding window of recently-touched files, bounded by CNFS_CACHE_SIZE_MB. It splits into two categories:
- • Pinned — files with an open handle (in-flight write or active read). Never evicted.
- • Evictable — files fully uploaded and closed. Evicted oldest-first when room is needed.
Eviction is just a rm on the local cache file — the authoritative copy is on the server, so the next read of that path falls through to a cache-miss → streaming GET. Drop a folder of fifty 50 MB photos onto a 20 MB cache and the cache holds the most-recently-touched files; the rest live on the server, fetched on demand.
Failure modes are bounded and observable
- Server unreachable during write: kernel
write(2)blocks on the ring buffer; after a 30 s grace,EIOpropagates up. The server-side staging file expires and is GC'd after 24 h. - Server unreachable during read: cache hits still serve. Cache misses return
EIO. - Server out of space: the upload PUT returns
ENOSPC; the client maps it toENOSPCon the writer. Local cache is unchanged. - Mid-upload network drop: the staging file on the server is deleted.
EIOon Flush/Release. No partial commit.
Implementation status
The model on this page is what cnfs is converging on. The current Linux client still uses an older 4 KiB block read-cache and a buffer-on-close write path — so today, dropping a file larger than your local cache budget will fail rather than stream. The migration to streaming uploads, file-level LRU, and bypass-cache reads is in flight; full status (per-feature, with file pointers) lives in docs/Designs/cache-and-disk-pressure.md.