Skip to content

CloudflareContainerBackend: /ws upgrade never completes on deployed Containers, while computerd's own /health and /ws dial-backs both succeed #114

Description

@yoanndefay

Symptom

getWorkspace(stub) never resolves against a deployed Worker + Durable Object using CloudflareContainerBackend. Every call surfaces:

WorkspaceTransportError: Workspace backend "container-shell": shell.exec failed after 1 reconnect
retry: initial=CloudflareContainerBackend(container-shell) [stage=ws]: /ws upgrade did not arrive
within 30000ms; last=CloudflareContainerBackend(container-shell) [stage=ws]: /ws upgrade did not
arrive within 30000ms

No exec() ever runs, so nothing downstream of the connection can be exercised.

Versions

  • @cloudflare/computer@0.2.1
  • ghcr.io/cloudflare/computer-computerd-linux-x64:0.2.1
  • wrangler@4.123.0

Reproduction

A Worker + Durable Object wired to match examples/container/src/index.ts as closely as I could read it: the ContainerBase / withWorkspaceContainer / CloudflareContainerBackend construction, egress: { mode: "direct" }, export { WorkspaceProxy }, override fetch() { return this.backend.handleFetch(request) }, observer: createCloudflareObserver({ tracing }), and an R2Bucket(env.Bucket) mount.

wrangler.jsonc matched to examples/container/wrangler.jsonc: instance_type: "standard-2", compatibility_date: "2026-05-26", rollout_active_grace_period: 0, rollout_step_percentage: [100], observability: { traces: { enabled: true } }.

Deployed with wrangler deploynot wrangler dev. I never reproduced or ruled this out locally; it only appears against real infrastructure. Then call any route that reaches getWorkspace(). It times out at [stage=ws] every time.

Both ends look healthy

  • wrangler containers list / containers info show the application reaching a healthy instance in every round.
  • wrangler tail on the Worker shows computerd completing round trips, paired and roughly 30 s apart — which lines up with the backend's own connectTimeoutMs retry cadence rather than any schedule of computerd's:
GET http://computer.internal/health - Ok
GET http://computer.internal/ws     - Ok

/health clearly works end to end. The anomaly is that /ws reaches the Worker and logs as Ok while the backend's #pendingUpgrade promise (packages/computer/src/backends/container/cloudflare-container.ts) never resolves.

A 200 (or any non-101) response to that GET would produce exactly this symptom and would still print as Ok in wrangler tail's compact line. I could not distinguish that from a 101 arriving too late without visibility into what handleFetch() actually returned — that is the single most useful thing someone with Cloudflare-side access could check.

Variables eliminated

Four independently deployed Application IDs, three fresh Durable Object identities per round.

Variable Tried Result
Stale DO state multiple fresh DO ids per round no change
Stale platform state full wrangler delete + containers delete + fresh deploy, new Application ID no change
Patience connectTimeoutMs 30 s → 45 s (~90 s wall clock across the built-in reconnect) no change
egress.mode "direct" (matched from the first draft) no change
instance_type standard-1 and standard-2 no change
compatibility_date 2026-08-01 and 2026-05-26 no change
Rollout policy absent, and 0 / [100] no change
observability shape { enabled: true } and { traces: { enabled: true } } no change
R2 mount absent and present no change

The last two were matched for a measurement rather than an argument — reading the source, createCloudflareObserver is a pass-through around tracing.enterSpan() with no network or async gating, and the mount only touches ws.fs, structurally separate from the /ws control channel. Neither was a plausible cause; both were tested anyway.

One secondary observation, offered as data rather than as the explanation: standard-2 scheduling looked less reliable than standard-1 on this account/region — longer to reach a healthy instance, and one of two max_instances slots more often stuck starting. But a request landing on a confirmed-healthy instance still failed identically.

What I could not rule out

Anything needing Cloudflare-side visibility: the actual status and body handleFetch() returned on the /ws GET that timed out, and whether ctx.container.interceptOutboundHttp behaves differently under real Firecracker-backed Containers than in the environment the SDK's own tests exercise. I note that the FUSE-deadlock regression test (packages/computerd/src/exec/runner.fuse.test.ts) runs against a privileged local Docker container, never a deployed Cloudflare Container — so a deployed-only transport difference would not be caught by CI.

I understand the package is explicitly preview and not for production; this is filed as a data point from a real deployment, not as a support request.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions