How the Workspace Crosses a Container Boundary
Cloudflare Computer’s central synchronization problem is simple to state: the Durable Object owns the authoritative Workspace, but a container needs its own filesystem view so ordinary Linux programs can use it. This chapter follows one command round from the DO-side write to the container-side execution and back again. The goal is to make clear why the system moves metadata and missing chunks rather than copying a whole disk, and why convergence after a retry is not the same thing as conflict resolution.
The container exists for a real reason. An agent may need a compiler, a native binary, a package manager, or a process environment that a Worker interpreter cannot provide. Those tools expect paths and files to behave like a local filesystem. Cloudflare Computer gives them that view through computerd and FUSE, but it does not surrender ownership of the data. The container is a working mirror; the Durable Object remains the durable center.
Why the container needs a mirror
If every container command had to call the Durable Object for every filesystem operation, ordinary Linux tools would become remote clients of a storage API. That would change the behavior of programs that expect a kernel-visible directory, positional reads, and normal process I/O. A FUSE mount lets node, a shell, a compiler, or a test runner use the familiar path inside the container.
The price is a second copy of the tree. The DO-side VFS is backed by SQLite in Durable Object storage. The container-side VFS is held by computerd, normally in the process-lifetime store unless a separate on-disk store is configured. The two sides do not share one revision counter or one transaction. They exchange changes over a capnweb WebSocket, and each side has to know how far the other side has progressed.
That second copy is not an accidental implementation detail. It is the boundary that buys Linux compatibility. It also creates the central obligation of the container backend: before a command runs, the container must see the DO-side state that the command is supposed to use; after the command runs, the DO must have a chance to absorb the changes the command made. Synchronization is the mechanism that makes the mirror useful without turning it into a competing source of truth.
The same distinction explains why the Worker shell and Worker JavaScript backends do not need this protocol. They call back to the host Workspace over Workers RPC and use the DO’s SQLite store directly. A container needs a mirror because it must look like a local Linux filesystem. A Worker backend can trade that local illusion for a direct capability path.
One command round, in order
A normal container-backed execution follows the sequence push → spawn → events or result → pull. Each stage has a different owner and a different reason for existing.
First, application code may write a file through Workspace.fs. That mutation is committed in the DO-side SQLite store and receives a revision. The container does not necessarily see it at the exact instant the write commits. The next push—or the pre-command push performed by the runtime—collects the changes that the selected container has not acknowledged.
The push is not a raw replay of every filesystem call. The sync layer coalesces changes to one latest candidate per path. If an agent rewrites the same configuration file five times between command rounds, the receiver generally needs the latest state, not five intermediate versions. A deletion is represented by a tombstone, because the absence of a live inode alone cannot tell the receiver that a path was deliberately removed.
File entries contain their mode, modification time, size, and ordered chunk hashes and sizes. They do not contain the bytes inline. Before transferring content, the sender and receiver compare hashes. The sender then streams only the missing chunk objects. The receiver stages those objects and applies the metadata that points the file at them. This order ensures that a file entry never arrives with a reference to content the receiver cannot assemble.
The receiver acknowledges the applied cursor only after its batch has settled. That acknowledgment is a safety gate for command execution. If the pre-command push fails, the command is not dispatched. Running a command against an empty or stale mirror and discovering the synchronization problem only afterward would be worse than delaying the command: the command might read the wrong source, compile the wrong files, or overwrite a result based on state the application never intended it to see.
Once the push is acknowledged, computerd runs the command. Through FUSE, writes under the mounted Workspace tree land in the container-side VFS and receive container-side revisions. Output is streamed through the command runner, independently of the file changes. A process can therefore produce stdout while it is still changing files; the output stream is not itself proof that the resulting filesystem state has reached the DO.
After the command finishes, the DO fetches changes from the container. It receives bounded batches of metadata, identifies the chunk hashes it lacks, fetches those objects, and applies the entries to its SQLite tree. The cursor advances only after the corresponding metadata has been applied. If the post-command pull cannot finish, the command result can still be complete while synchronization is pending. The command is not rerun merely because its result-to-Workspace handoff failed; a later pull resumes the transfer.
This separation is also the basis for retry discipline. A cursor-driven sync can be repeated because the receiver can recognize content and metadata it already applied. A shell command is different. If the connection fails after the container may have accepted the spawn request, the backend cannot know whether the command ran. Replaying it could duplicate an external side effect, create two commits, send two requests, or overwrite a file twice. The container backend retries command setup only when it has evidence that dispatch never occurred; it treats an ambiguous post-dispatch failure as a failure to report, not an invitation to run the command again.
Why hashes and coalescing reduce the work
The filesystem model from the previous chapter makes the transfer selective in two independent ways.
Coalescing reduces the number of state descriptions. It is useful when the path is what matters and intermediate states do not. An agent that edits a JSON file, formats it, and then fixes one field has generated several local revisions, but a container starting its next command usually needs only the final file. The wire can carry one entry for that path.
Content addressing reduces the number of byte payloads. If two paths contain identical chunks, the blob store identifies them with the same SHA-256 hash. A receiver that already holds that hash does not need the bytes again. The same applies when an edited file retains unchanged fixed windows: the metadata for the file changes, but the unchanged chunk objects can remain local to both sides.
This distinction matters at different scales. Coalescing helps a burst of writes to the same path. Content addressing helps repeated content across paths and across time. Together they turn a sync round into “describe the latest tree differences and supply the objects the receiver lacks,” rather than “serialize every operation and resend every byte.”
The benefit has a boundary. The current filesystem uses fixed 512 KiB windows. A tail append usually leaves earlier hashes stable, but inserting one byte near the beginning can shift the contents of every later window. A file that is logically almost unchanged may therefore look extensively changed at the chunk level. The protocol is incremental with respect to its representation; it does not perform semantic diffing or use content-defined boundaries today.
The same rule applies to directories, but with a different cost. A local directory rename can preserve an inode relationship efficiently. The synchronization wire has no rename opcode. Instead, it publishes the live entries at their new paths and tombstones at the old paths. For a large subtree, that means work proportional to the number of moved entries. The design accepts that cost because the receiver can bootstrap from final state, even if it never held the old path, and can reapply the same entries without replaying a history of operations.
Final state is easier to recover than operation history
An operation such as “rename this directory” assumes something about the receiver’s past. If the receiver never had the source path, a relink instruction has no useful meaning. A state-based entry can say what should exist at the destination and what should no longer exist at the source. That statement works for a cold-start receiver, an incrementally caught-up receiver, and a receiver reprocessing a batch after a crash.
The trade is that the wire describes the resulting tree, not the exact historical state at every revision. The VFS keeps current content rather than a complete content history. If a path changes again while a change stream is being prepared, the stream may defer the newer state to a later cursor rather than freeze an old copy of the bytes. The cursor does not advance past the revision that would need to be redelivered, so convergence is preserved without pretending the protocol is a snapshot database.
Replay safety is specific to the kind of entry. A file that already has the same manifest can be ignored. A directory can be compared by mode, and a symbolic link by target and mode. A tombstone needs more care: if the receiver recreated the path after the tombstone’s revision, replaying the old deletion would destroy newer information. The stale delete is dropped, while the newer recreation remains a change that can move in the other direction.
Read-only mounts create another deliberate outcome. If an incoming container-side entry targets a read-only mount, the receiver does not apply it to the authoritative mounted path. It records the path as skipped and advances the cursor so an entry that can never succeed does not block every later change. The skip is surfaced to the caller and recorded durably, making the refusal observable rather than silently turning the sync operation into an unexplained no-op.
These rules aim at a convergent final tree under the protocol’s intended replay model. They do not preserve every intermediate edit, and they do not provide collaborative merge semantics. That distinction becomes important when considering more than one writer.
Cursors make a large transfer restartable
A small sync can often finish inside one invocation. A large tree cannot safely depend on that assumption. A Durable Object may be evicted between two calls, an alarm may reach a time limit, or a network stream may fail after part of the data has been applied. Cloudflare Computer therefore stores progress in SQLite rather than trusting an in-memory JavaScript iterator to survive.
The fetch cursor has two coordinates: a revision and, when necessary, a path within that revision. The path component matters because one mutation can touch many paths at the same revision—for example, a directory rename or recursive deletion. A scalar revision watermark would have to replay the whole revision after an interruption. (revision, path) lets the next operation resume within that revision without inventing intermediate revisions that never represented a complete committed state.
The restartable engine fixes a target for an operation and processes one bounded block at a time. It plans the block, transfers its objects, applies its entries, advances the durable cursor, and only then yields progress. Recreating the iterator reads the operation row and watermark again. The JavaScript iterator may be gone; the operation’s durable position is not.
The engine also chooses between entry mode and pack mode for the operation as a whole. A window at or above 20,000 entries or an estimated 100 MiB payload uses a compressed change pack that carries entries and their unique objects together. Smaller windows can use individual entries with object probes and streams. Fixing the mode prevents a replay from changing shape merely because a later request observed a different amount of work.
The application, not the library, owns the schedule. A caller can drive one block from an alarm, several blocks within a request, or another application-controlled mechanism. If it stops calling, the pending operation remains pending. Durable cursors make resumption safe; they do not create an alarm or guarantee that a future invocation will happen by itself.
Convergence is not conflict resolution
Suppose two containers share one Workspace. Each reads plan.md, makes a different edit, and pushes without seeing the other’s change. The DO can apply both batches without holding a partially written file, but it does not merge their contents. Whichever batch arrives later at the authoritative store wins at the sync granularity, and the earlier edit can be overwritten without an error to either agent.
This is acceptable for the common one-agent, one-container pattern. It is also structurally safe when different agents own different subtrees or when files are written once and then read. It is not safe for a shared counter, a common plan, or a read-modify-write cycle performed independently by multiple containers. Two agents can both read 5, both write 6, and leave the Workspace at 6; object integrity does not imply that the intended combined update occurred.
The practical consequence is straightforward: use an explicit handoff when changing writers, pull before beginning work that depends on another writer’s final state, and partition namespaces when independent agents must work concurrently. Shared coordination state belongs in an interface designed for coordination, not in a Workspace file that silently follows last-writer-wins rules.
The synchronization protocol therefore solves a focused problem. It keeps a container-backed execution environment supplied with durable Workspace state, transfers only the content the receiver needs when the representation allows it, and resumes interrupted work from committed progress. It does not turn the container into a second authority, preserve every historical operation, or make the Workspace a shared CRDT.
That leaves a more exact meaning of durability to carry forward: a file can be durably committed on the DO, mirrored in a container, acknowledged by the receiver, and still be part of an execution whose output or later pull has not settled. The next step is to separate those states explicitly and use them to compare the execution backends that can surround the same Workspace.