FUSE Write Model in Cloudflare Computer: Buffered Writes and the Commit Process
Cloudflare Computer implements the FUSE write model using an in-process write-buffer cache that defers persistence to SQLite-backed chunk stores until the last file handle is released.
Cloudflare Computer's DOFS (Durable Object File System) mounts a FUSE filesystem that employs a write-back caching strategy to optimize performance. When applications write to files through the FUSE mount, data lands in an in-memory WriteBufferEntry rather than immediately hitting the database. This design minimizes expensive SQLite transactions and ensures atomic file creation, but requires a sophisticated commit process when files are closed.
How the Write Buffer Cache Works
The FUSE write model centers on an in-process cache that intercepts all write operations before they reach the persistent chunk store.
Opening Files and Allocating Buffers
When a file is opened through the FUSE interface, the system allocates a write buffer via openWriteBufferForCreateSync (for new files) or openWriteBufferSync (for existing files) in packages/dofs/src/provider.ts.
For brand-new files, the system generates a synthetic negative inode using allocatePendingInode. This temporary identifier allows the buffer to track the file before it exists in the SQLite database. The buffer entry stores metadata including file path, mode, and a reference count.
Handling Write Operations
All write operations—including write, writev, pwrite, and truncate—target the buffer rather than the database. In packages/dofs/src/fs/writeFile.ts, the writeRangeSync function implements this logic:
- Calls
ensureCapacityto grow the internal buffer (entry.buf) as needed - Writes bytes at the specified offset
- Updates
entry.sizeto reflect the new file size - Sets
entry.dirty = trueand records thedirtyPathfor tracking
This buffered approach allows multiple small writes to coalesce into a single database transaction.
Managing Multiple File Handles
The buffer cache maintains an openCount field (defined in packages/dofs/src/fs/writeBuffer.ts) to track how many file descriptors reference the buffer. Each open operation increments this count, while each release (close) decrements it.
The buffer is never flushed while openCount > 0, ensuring that concurrent writers see each other's modifications and that partial writes never reach the database prematurely.
The Commit Process on File Release
The actual persistence logic triggers in releaseWriteBufferSync when the final handle to a file is closed (openCount reaches zero).
Pending Create vs. Existing File Handling
The system distinguishes between two commit paths:
Pending-Create Buffers (new files): When the inode does not yet exist in SQLite, the release transaction performs an atomic INSERT operation that creates:
- A new node record in the SQLite database
- The directory entry (dirent) linking the path to the inode
- The buffered chunks containing the actual file data
After successful insertion, promotePendingToInode converts the synthetic negative inode into a permanent database-assigned inode.
Existing File Buffers: For files already present in the database, the commit process calls writeChunk to persist buffered bytes to new chunks in the SQLite store, then updates the inode's size and mode metadata.
Atomic SQLite Transactions
Both commit paths execute within single SQLite transactions to guarantee consistency. After the transaction commits successfully, the system evicts the buffer entry via deleteWriteBuffer, freeing the memory and completing the FUSE write lifecycle.
Consistency and Read-While-Write
To maintain filesystem consistency, operations that could bypass the buffer—such as rename, link, or unlink—first invoke flushPendingByPath or flushPendingUnderNode (implemented in packages/dofs/src/provider.ts). These functions force any dirty buffers for affected paths to commit immediately, ensuring that subsequent operations see the most recent data.
For read operations, readFileSync and the FUSE read path check getWriteBuffer before querying the database. If a dirty buffer exists, the read returns a snapshot of the buffered bytes, ensuring readers see uncommitted writes without waiting for the file to close.
Code Example: Buffered Write Lifecycle
import { DOFSProvider } from '@cloudflare/dofs';
const p = new DOFSProvider(db);
// Create a new file with a pending-create buffer
p.openWriteBufferForCreateSync("/new.txt", { mode: 0o644 });
// Write data to the buffer (not yet persisted)
p.writeRangeSync("/new.txt", Buffer.from("hello world"), 0);
// Close the file – this triggers the atomic commit to SQLite
p.releaseWriteBufferSync("/new.txt");
// Read back from the persisted chunks
const data = p.readFileSync("/new.txt", "utf8"); // "hello world"
Updating an existing file follows the same pattern but uses openWriteBufferSync instead:
// Open existing file for buffered writing
p.openWriteBufferSync("/existing.txt");
p.writeRangeSync("/existing.txt", Buffer.from("ABC"), 5);
p.releaseWriteBufferSync("/existing.txt");
Summary
- Write-back buffering: Cloudflare Computer uses an in-process
WriteBufferEntrycache to batch writes, reducing SQLite transaction overhead. - Reference counting: The
openCountfield prevents premature flushing while multiple file handles reference the same buffer. - Atomic commit on close: Data persists to the SQLite-backed chunk store only when the last handle closes (
releaseWriteBufferSync). - Two-phase commit: New files use a "pending-create" path with synthetic inodes and
promotePendingToInode, while existing files update chunks directly. - Consistency hooks: Rename, link, and unlink operations force buffer flushes via
flushPendingByPathto prevent visibility gaps. - Dirty reads: Read operations check the buffer cache first, allowing visibility of uncommitted writes before the commit finishes.
Frequently Asked Questions
When does Cloudflare Computer actually write data to SQLite?
Data commits to SQLite only when the last file descriptor is closed and releaseWriteBufferSync is invoked. At that point, if the openCount reaches zero, the system executes an atomic transaction that writes the buffered chunks to the database and either inserts a new inode (for pending creates) or updates an existing one.
How does the system handle multiple processes opening the same file?
The buffer cache tracks concurrent access via the openCount field defined in packages/dofs/src/fs/writeBuffer.ts. Each open operation increments the count, and the buffer remains in memory (and dirty) until the count reaches zero. This ensures that all writers see a consistent view of the file data before any commit occurs.
What happens if a file is renamed while it has buffered writes?
Operations like rename trigger flushPendingByPath or flushPendingUnderNode in packages/dofs/src/provider.ts before executing. These functions force an immediate commit of any dirty buffers associated with the affected paths, ensuring that the rename operation sees the most recent data and maintains filesystem consistency.
Are writes visible to other readers before the file is closed?
Yes. The read path in packages/dofs/src/provider.ts first checks getWriteBuffer for dirty entries. If a buffer exists, reads return a snapshot of the buffered bytes rather than querying the SQLite store. This allows concurrent readers to see uncommitted writes without waiting for the file release and commit process to complete.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →