Repository navigation
Description
Describe the bug
The sync coalescer performs work for the entire pending change set before yielding its first entry. A downstream block limit therefore does not bound this initial planning work.
We investigated repeated container-exec failures with @cloudflare/computer@0.4.1. Six invocations on the same SQLite-backed Durable Object reported exceededCpu at 32,500 ms and an instance reset. Each invocation included the same large batch of VFS path-resolution queries before failure.
Evidence boundary: the eager planning behavior is confirmed in source. The telemetry makes it a strong candidate contributor to these CPU failures, but we do not have per-stage CPU profiling or a standalone reproducer that proves it accounts for the entire CPU budget. We also cannot establish from these traces whether the shell command had started. A Durable Object reset is not evidence that the container restarted.
Source path
Incident package: @cloudflare/computer@0.4.1. We also checked the following upstream sources while preparing this report:
- coalesce.ts:
coalesceChangescallsdb.allfor the full touched-inode revision range, resolves all touched inodes throughpathsOfMany, builds the candidate map, merges tombstones and sorts the candidates before the firstyield. - paths.ts:
pathsOfManybatches 512 inodes per recursive SQL query, but synchronously processes every batch before returning.
In the inspected 0.4.1 bundle, the exec path pushes changes before spawnShell. Its mode-selection probe and block entry/byte limits consume the coalescer after this eager work. Smaller output blocks therefore do not by themselves bound the work before the first block.
Observed behavior
Sanitized aggregate measurements from six failed invocations:
| Measurement | Observed value |
|---|---|
| Invocation active CPU | 32,500 ms each |
| Invocation wall time | Approximately 37.7–40.2 seconds |
| Path-resolution query batches | 449 per invocation |
| Billed rows read for those path batches | Approximately 10.34 million per invocation |
| Earlier touched-range query | Approximately 229,500 billed rows read |
The SQL templates were matched to coalesceChanges → pathsOfMany → collectBatch. The large enumeration repeated on subsequent attempts. Other lightweight operations on the same object could succeed.
rows_read is a platform billing metric, not a unique-node count or a CPU measurement. We did not recover the incident cursor values and are not claiming cursor corruption. We have omitted workspace contents, paths, object IDs and private trace identifiers.
Expected behavior
Preparing the first sync block should have a bounded work budget that can make durable progress on a large change set under the Durable Object CPU limit. Reconnection should not force unbounded planning to restart before any progress can be acknowledged.
Reducing the wire block size should either bound the preparatory work too, or the API should clearly expose the separate planning cost and a supported way to bound it.
Steps to reproduce / current reproduction limits
The observed trigger was a container-backed workspace with a large pending change set, followed by a minimal shell-command request through workspace.runtime.exec. Repeated attempts encountered the measurements above.
We do not yet have a portable, self-contained reproduction of the exact deployed CPU failure. The source-level eager-work boundary can be isolated as follows:
- Use the existing real SQLite test harness to create a large nested tree and retain a cursor preceding those writes.
- Instrument database calls, then request only the first entry from
coalesceChanges(db, cursor). - Observe that the full touched range and all path-resolution batches are processed before that first entry is returned.
- For a platform reproduction, prepare equivalent data across bounded setup requests in a disposable SQLite Durable Object with the container backend, then issue a minimal exec and record platform CPU plus sync-planning/spawn stage markers.
These are suggested isolation steps, not a claim that this exact standalone fixture has already reproduced the CPU reset. Tree depth, pending revision range and peer cursor state should be reported with any reproduction.
Environment
- Incident SDK and computerd image version: 0.4.1.
- Cloudflare Workers, SQLite-backed Durable Object, container execution backend.
- Deployed compatibility date:
2026-07-15. - No explicit
limits.cpu_msoverride. - Source inspection during this report: upstream commit
61f5aeae56e8e7ed75dddf235a9e8983508a0b39. We have not reproduced the platform failure on that newer commit. - The consumer had local patches for stream decoding, cancellation and read-handle behavior; the incident patch did not alter sync planning or schema.
Possible direction
Could the existing sync planner bound candidate reading, path reconstruction and sorting before the first output block, using its existing cursor/target mechanism? Any change needs to preserve same-revision path ordering, hard links, tombstones, concurrent changes and lost-acknowledgement recovery; simply limiting the initial SQL result could silently skip changes.
Related: #67 concerns retained tombstones. This report concerns eager planning over the current pending live-node set as well, so pruning old tombstones alone would not address the boundary described here.
Submitting an issue as requested by CONTRIBUTING.md. Maintainer guidance on a supported bounded-planning approach would be appreciated.
Activity
aron-cf commentedon Oct 12, 2026
Hey, thanks for the bug report. The current implementation has been running on a wide variety of workloads for the past six months without hitting CPU limits. I'm not saying that there aren't potential issues here. But I'm going to need a more concrete reproduction in order to action this ticket. I'll close it for now, please re-open if you can provide more details.
ian-pascoe commentedon Oct 22, 2026
We hit what looks like this in production, and have the telemetry you asked for. Happy to provide more if it helps.
Setup:
@cloudflare/computer0.5.1, computerd 0.5.1 (ghcr.io/cloudflare/computer-computerd-linux-x64:0.5.1),ContainerBackend(scheduling_policy: "durable_object") alongsideWorkerShellBackend,ignoreset (**/node_modules,**/.venv, caches), computerd's default in-memory store.Trigger: the Workspace's SQLite filesystem held 96,320 files (1.5 GB) in only 406 directories. 95,729 of those files were a pnpm content-addressed store at
/workspace/.pnpm-store/v11/files/{00..ff}, about 374 files per directory. pnpm put the store at the root of the FUSE mount because the home directory is on a different filesystem, and ourignorelist didn't cover it. With the in-memory store, every new container comes up empty,reconcileWatermarksresets the push cursor to 0, and the first command waits for a push of the whole tree.What Workers Observability showed (one Durable Object, Workers traces):
durable_object_storage_execstatements in roughly three minutes, under the computerd/apiWebSocket invocation. That invocation reported 164 s wall and 46.9 s CPU, and the container was then replaced.SELECT inode, type, mode, mtime, size, link_target FROM vfs_nodes WHERE inode = ?SELECT child_inode FROM vfs_dirents WHERE parent_inode = ? AND name = ?SELECT rev FROM vfs_nodes WHERE inode = ?SELECT hash, size FROM vfs_chunks WHERE inode = ? ORDER BY idxtargets(inode)/walk(target, link, depth, name, parent_inode)CTESELECT bytes FROM vfs_blob_bytes WHERE hash = ?__getWorkspaceStub) waited 26–29 s with about 0 ms CPU of their own. One RPC of ours that walks a few directories throughworkspace.fs(normally 2 ms) reported 103 s wall and 79.5 s CPU. We can't attribute that CPU between our call and the concurrent push./apiWebSocket invocation held for 4.76 h (47 s CPU) ended withDurable Object connection closed because the object was reset.The object's alarms in the half hour before took 600 s and 39 s of wall time (normally about 2 ms).To users, simple commands "hang": they queue behind the push.
What fixed it for us:
**/.pnpm-storeinignore, so pnpm's store stays on the container's disk. pnpm can still hard-link it into an ignorednode_modules; we checked that on a deployed container.COMPUTERD_DB=/var/lib/computerd/state.db, so a container restored from a snapshot keeps its sync positions. Measured on a deployedstandard-2: a 3,001-file Workspace restored from its idle snapshot was pushed 1 entry in 1.1 s; launched from the image it was pushed 3,087 entries in 7.2 s.Two things that would have helped, in case they're useful:
ignoreafter files were synced there is still pushed to every new container, into computerd's store where commands never see it.coalesceChangesalready takes anignoreoption, butWorkspacedoesn't pass one for pushes.rm -rof the tree in the Durable Object, which records onevfs_changestombstone per path. With Prune acknowledged tombstones from vfs_changes so the table stops growing without bound #67 open, a fresh container would then be sent 96k deletes on every launch.