How to Configure a Workspace with Multiple Backends and Lazy Backend Connections in Cloudflare Computer
A Workspace in Cloudflare Computer supports multiple backends by registering them in the WorkspaceOptions.backends array and enables lazy connections by deferring RPC target initialization until the first operation, with each backend maintaining independent sync state namespaced by a stable backend ID.
The Cloudflare Computer repository provides a host-side façade called Workspace that orchestrates execution across distributed environments. When you configure a workspace with multiple backends and lazy backend connection capabilities, you gain fine-grained control over resource allocation while maintaining isolated sync cursors for each execution target in the underlying dofs database.
Understanding Workspace Architecture
Multiple Backend Registration
In packages/computer/src/workspace.ts, the Workspace class accepts a backends array in its constructor options. Each entry must include a stable id and a handle implementing the backend interface. The first element in this array becomes the default backend for operations that do not specify a target.
The underlying dofs database schema in packages/dofs/src/schema/sync.ts adds a backend column to the _vfs_watermark and _vfs_fetch_cursor tables. Helper functions in packages/dofs/src/sync/watermarks.ts read and write these watermarks with an optional backend argument, defaulting to DEFAULT_BACKEND_ID ("default").
Lazy Connection Mechanics
The lazy behavior is implemented in packages/computer/src/stub.ts through the WorkspaceStub class. The connect() method checks for an existing connection before creating an RpcTarget instance. This deferred initialization means no network sockets or Durable Object alarms are established until the first RPC call (such as pull, push, or exec).
The sync driver in packages/rpc/src/sync-driver.ts forwards the optional backend argument to sync functions, preserving lazy semantics for each backend independently.
Configuring Multiple Backends
To register multiple backends, import the appropriate backend factories and pass them to the Workspace constructor:
import { Workspace } from "@cloudflare/computer";
import { createContainerBackend } from "@cloudflare/computer/backends/container";
import { createWorkerBackend } from "@cloudflare/computer/backends/worker";
const container = await createContainerBackend({ /* configuration */ });
const worker = await createWorkerBackend({ /* configuration */ });
const ws = new Workspace({
storage: ctx.storage,
backends: [
{ id: "container", handle: container },
{ id: "worker", handle: worker },
],
});
The first entry ("container" in this example) serves as the default backend. When you call ws.runtime.exec() without specifying a backend ID, it routes to this default.
Each backend maintains isolated sync state. A pull operation against "container" only updates watermarks namespaced to that ID, leaving "worker" cursors untouched. This isolation prevents cross-backend contamination and allows independent progress tracking.
Implementing Lazy Backend Connections
Lazy connections optimize resource usage by deferring network establishment until the first actual operation:
// Workspace created - no network traffic yet
const ws = new Workspace({
storage: ctx.storage,
backends: [{ id: "container", handle: container }],
});
// First exec triggers lazy connect via WorkspaceStub.connect()
await ws.runtime.exec("container", ["node", "-e", "console.log('hello')"]);
// Subsequent calls reuse the established channel
await ws.runtime.exec("container", ["node", "-e", "console.log('again')"]);
The same pattern applies to explicit sync operations:
// Triggers connection only for the "worker" backend
await ws.pull("worker");
// Reuses existing connection
await ws.push("worker");
This architecture provides resource efficiency—unused backends incur no connection overhead—and failure isolation, where one unavailable backend does not block operations targeting other backends.
Advanced Configuration Examples
Three-Backend Setup
You can combine container, worker, and custom RPC backends in a single workspace:
import { createRpcBackend } from "@cloudflare/computer/backends/rpc";
const container = await createContainerBackend({ /* … */ });
const worker = await createWorkerBackend({ /* … */ });
const rpc = await createRpcBackend({ url: "https://example.com/rpc" });
const ws = new Workspace({
storage: ctx.storage,
backends: [
{ id: "container", handle: container },
{ id: "worker", handle: worker },
{ id: "rpc", handle: rpc },
],
});
Backend-Specific Operations
Target specific backends explicitly to control where code executes:
// Pull only from the RPC backend
await ws.pull("rpc");
// Execute on default backend (first entry)
await ws.runtime.exec(["bash", "-c", "echo default"]);
Inspecting Per-Backend Sync State
Monitor individual backend progress using the dofs database helpers:
import { readFetchCursor, readWatermark } from "@cloudflare/dofs";
const db = ws.db;
console.log(
"Container cursor:",
readFetchCursor(db, "container"),
"Worker cursor:",
readFetchCursor(db, "worker")
);
Summary
- Register multiple backends by passing an array to
WorkspaceOptions.backendsinpackages/computer/src/workspace.ts, where the first entry becomes the default. - Each backend receives a stable ID that namespaces sync state in the
dofsdatabase via thebackendcolumn in_vfs_watermarkand_vfs_fetch_cursortables. - Lazy connections are handled by
WorkspaceStub.connect()inpackages/computer/src/stub.ts, which initializesRpcTargetinstances only upon first RPC invocation. - The sync driver in
packages/rpc/src/sync-driver.tspreserves per-backend lazy semantics by forwarding the optionalbackendargument. - This design ensures resource efficiency and failure isolation across distributed execution environments.
Frequently Asked Questions
How do I set a default backend when configuring multiple backends?
The default backend is automatically assigned to the first element in the backends array passed to the Workspace constructor. Any operation that omits the backend ID parameter, such as ws.runtime.exec(), will route to this default. You can override this behavior by explicitly passing a backend ID string as the first argument to most workspace methods.
What happens to sync state when using multiple backends?
Each backend maintains completely isolated sync state. The dofs schema stores watermarks and fetch cursors with a backend column identifier, ensuring that a pull or push operation against one backend never interferes with another's progress tracking. This isolation is managed by helper functions in packages/dofs/src/sync/watermarks.ts that accept an optional backend parameter.
When exactly does a lazy backend connection get established?
A connection is established on the first RPC call targeting that specific backend, such as exec, pull, or push. The WorkspaceStub class in packages/computer/src/stub.ts checks for an existing connection in its connect() method and only initializes the RpcTarget when needed. If a backend is never invoked, it remains disconnected and consumes no network resources or Durable Object alarms.
Can I mix different backend types in the same workspace?
Yes, you can combine container backends, worker backends, and custom RPC backends within a single Workspace instance. Each backend type implements the same interface and registers with a unique ID, allowing you to route specific workloads to the most appropriate execution environment while maintaining unified sync management through the dofs database.
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 →