Push vs Pull Sync in Cloudflare Computer: When to Use Each Operation
Push sync streams changes from the Durable Object to the container before execution; pull sync retrieves container writes back to the Durable Object after execution.
Cloudflare Computer maintains two copies of every workspace: a SQLite-backed copy in the Durable Object (DO) that persists across restarts, and a FUSE-mounted copy in the ephemeral container that only lives for the duration of a process. The push and pull sync operations keep these copies consistent using incremental, bidirectional synchronization with monotonic revision watermarks. This guide explains how each operation works, where it's implemented in the source code, and exactly when to use each one.
What Push Sync Does (DO → Container)
Push sync transfers every ChangeEntry that the DO has generated since the last successful push. The operation coalesces multiple writes to the same path—five writes to one file become a single entry on the wire.
How Push Sync Works
The implementation in [packages/rpc/src/sync-driver.ts](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/sync-driver.ts) follows this sequence in the pushOnce function:
- Probe the container for chunk hashes it already owns via
hasObjects - Stream missing chunks via
pushObjects - Stream change entries via the
pushRPC - Confirm application: The container returns an
appliedPushCursorthat must cover the revision the DO claimed to push; the DO then advances itspushRevwatermark
// Push DO changes to container before execution
import { pushOnce } from "@cloudflare/computer/rpc";
// db: local DO database
// remote: container RPC stub
await pushOnce(db, remote);
// Or use the high-level workspace API
await workspace.push(); // also pulls pending changes first
When to Use Push Sync
- Before
exec: Push pending changes so the container sees the latest state when the command runs - Explicit synchronization: Force a push without executing a command, such as after batch writes from a script
What Pull Sync Does (Container → DO)
Pull sync retrieves every change the container has produced since the last successful pull. Each entry carries the hashes of file chunks it references; the DO probes for missing chunks and fetches only those.
How Pull Sync Works
The pullOnce and pullOnceImpl functions in [packages/rpc/src/sync-driver.ts](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/sync-driver.ts) execute this flow:
- Fetch changes: Call
fetchChanges({ after })to receive a stream ofChangeEntrys pluscurrentCursorandappliedPushCursor - Batch and apply: Process up to
PULL_BATCH_SIZE(256) entries at a time—probe for missing chunks withhasObjects, fetch them withfetchObjects, then apply the batch viaapplyChanges - Advance cursor: After each batch, write the new fetch cursor with
writeFetchCursorIfAhead
// Pull container writes back into the DO
import { pullOnce } from "@cloudflare/computer/rpc";
await pullOnce(db, remote);
// Or use the high-level workspace API
await workspace.pull(); // used after exec returns
When to Use Pull Sync
- After
exec: The container may have written files via FUSE; pull those writes to persist them in SQLite - Explicit synchronization: Catch up the DO state without waiting for the next command execution
The Pull-Then-Push Pattern: tick
The standard sync loop performs pull first, then push in a single tick operation. Pulling first ensures remote changes are applied before the DO calculates what still needs to push, preventing redundant uploads of identical entries.
// Full sync tick (pull then push)
import { tick } from "@cloudflare/computer/rpc";
const { pulled, pushed } = await tick(db, remote);
console.log(`Applied ${pulled.applied} entries, pushed ${pushed} entries`);
Use tick for deterministic synchronization in tests or custom workflows where you need complete reconciliation in one step.
Push vs Pull: Decision Reference
| Situation | Operation | Method |
|---|---|---|
| Wrote files inside the container (build artifacts, tool output) | Pull | workspace.pull() or pullOnce |
Mutated files through the DO API (ws.fs.writeFile) |
Push | workspace.push() or pushOnce |
About to run exec that reads files |
Push then execute | pushOnce or workspace.push() |
Just finished exec that may have written files |
Pull | workspace.pull() or pullOnce |
| Need deterministic full sync | Both | tick() |
Key Implementation Details
Watermark Tracking
The DO maintains two critical counters in its SQLite schema ([packages/dofs/src/schema/sync.ts](https://github.com/cloudflare/computer/blob/main/packages/dofs/src/schema/sync.ts)):
pushRev: Last revision successfully pushed to the containerfetchCursor: Last revision successfully pulled from the container
These watermarks enable incremental sync—only deltas transfer across the network.
Coalescing and Batching
- Push: Per-path coalescing collapses multiple writes to identical paths
- Pull: Fixed batch size of 256 entries (
PULL_BATCH_SIZE) balances throughput with memory usage
Summary
- Push sync (DO → container) prepares the container for execution by streaming pending DO changes first
- Pull sync (container → DO) persists container output by retrieving FUSE-mounted writes back to durable SQLite storage
- The
tickfunction combines both operations in the optimal order (pull, then push) for complete reconciliation - Watermarks (
pushRev,fetchCursor) enable efficient incremental synchronization without full state transfers
Frequently Asked Questions
What happens if I call push without pulling first?
You risk overwriting container writes that haven't been persisted to the DO yet. The DO's pushRev watermark may claim revisions that the container has already modified locally, causing synchronization conflicts. Always pull first unless you're certain the container hasn't written anything.
How does Cloudflare Computer handle large files during sync?
The sync protocol operates on content-addressed chunks, not whole files. Both push and pull first probe (hasObjects) which chunks the remote side already possesses, then transfer only missing chunks (pushObjects/fetchObjects). This deduplication happens automatically regardless of which operation you're running.
Can I use workspace.push() and workspace.pull() in my own scripts?
Yes. These high-level APIs wrap the lower-level pushOnce and pullOnce functions from @cloudflare/computer/rpc. Note that workspace.push() internally performs a pull first to merge any pending container changes, while workspace.pull() performs only the pull operation.
Where is the sync protocol documented architecturally?
The design document at [docs/02_sync_protocol.md](https://github.com/cloudflare/computer/blob/main/docs/02_sync_protocol.md) covers watermark reconciliation, the push/pull lifecycle, and protocol invariants in detail. The reference implementation lives in [packages/rpc/src/sync-driver.ts](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/sync-driver.ts), with comprehensive tests in [packages/rpc/src/sync-driver.test.ts](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/sync-driver.test.ts).
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 →