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:

  1. Probe the container for chunk hashes it already owns via hasObjects
  2. Stream missing chunks via pushObjects
  3. Stream change entries via the push RPC
  4. Confirm application: The container returns an appliedPushCursor that must cover the revision the DO claimed to push; the DO then advances its pushRev watermark
// 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:

  1. Fetch changes: Call fetchChanges({ after }) to receive a stream of ChangeEntrys plus currentCursor and appliedPushCursor
  2. Batch and apply: Process up to PULL_BATCH_SIZE (256) entries at a time—probe for missing chunks with hasObjects, fetch them with fetchObjects, then apply the batch via applyChanges
  3. 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 container
  • fetchCursor: 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 tick function 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →