Critical RPC Patterns Developers Need to Be Aware of in Cloudflare OS

Cloudflare OS uses Cap’n Web RPC over persistent WebSockets to enable capability-based communication between front-end UI, back-end workers, and gatekeeper services, requiring developers to master stub lifecycle management, promise pipelining, and interface validation to prevent resource leaks and eliminate unnecessary network round-trips.

Cloudflare OS is an open-source platform that orchestrates complex interactions between distributed workers and user interfaces through a unified RPC layer. Understanding the critical RPC patterns developers need to be aware of in Cloudflare OS is essential for building performant applications that avoid common pitfalls like memory leaks in Durable Objects or excessive latency. These patterns are implemented across the codebase, with core interface definitions located in packages/workshop-shared/src/api.ts and supporting logic in the gatekeeper and backend packages.

Capability Design Patterns

Extend RpcTarget for Network-Visible Interfaces

Every public API in Cloudflare OS must be declared as an interface that extends RpcTarget. This design guarantees that methods are reachable over the network and can be used as first-class capabilities.

In packages/workshop-shared/src/api.ts, the PublicApi interface follows this pattern at line 48, ensuring that all exposed methods can be invoked remotely. Similarly, AuthenticatedApi and gatekeeper interfaces adhere to this requirement, forming the foundation of the capability-based security model.

Return RpcStub for First-Class Capabilities

Methods that create new capabilities must return RpcStub<T> rather than concrete implementations. This allows the stub to be pipelined and passed around without awaiting a network round-trip, effectively treating remote references as local objects.

For example, startGatekeeperLogin returns RpcStub<LoginAttempt> (line 68 in api.ts), while openGadget returns RpcStub<Overseer> (line 78). These stubs represent live capabilities that can be invoked immediately or stored for later use.

Performance Optimization Patterns

Leverage Promise Pipelining for Zero-Round-Trip Calls

Cloudflare OS RPC calls accept RpcStub parameters directly, sending the stub "as-is" and resolving it on the server before execution. This enables "one-round-trip" interactions that dramatically reduce latency for chained operations.

The openGadget signature in packages/workshop-shared/src/api.ts demonstrates this pattern by accepting a configureObservers argument of type RpcStub<ObserverConfigCallback>. When calling openGadget, you can pipeline subsequent method calls on the returned RpcStub<Overseer> without waiting for the initial call to complete, as shown in packages/workshop-backend/src/server.ts.

Resource Management Patterns

Dispose Stubs to Prevent Durable Object Leaks

Every stub implements [Symbol.dispose]() and must be disposed when the capability is no longer needed. A dangling stub keeps a Durable Object alive on the server, causing resource leaks.

In the front-end, implement disposal in React effects:

useEffect(() => {
  return () => {
    stub[Symbol.dispose]();
  };
}, [stub]);

This pattern is critical in packages/workshop-frontend/src/useWorkspaceOpen.ts, where components must clean up RpcStub<Overseer> instances when unmounting to release server-side resources.

Store Stubs Safely in React State

Never store a raw RpcStub directly in React state. Because a stub is a callable object, React’s state setter treats functions specially and may attempt to invoke the stub as a state updater, which throws an error.

Wrap the stub in an object before storing it:

const [overseer, setOverseer] = useState<{ stub: RpcStub<Overseer> } | null>(null);

This pattern appears in packages/workshop-frontend/src/useWorkspaceOpen.ts, preventing React from misinterpreting the callable stub as a functional state update.

Communication Patterns

Implement Observer-Configuration Flow

When a non-owner opens a gadget requiring additional gatekeeper accounts, the server calls ObserverConfigCallback.configure and expects an array of ObserverAccountChoice. This pattern allows the UI to prompt users just-in-time for missing credentials without extra round-trips.

The types ObserverConfigCallback, ObserverBindingNeed, and ObserverAccountChoice are defined in packages/workshop-shared/src/api.ts (lines 78-94). The server invokes the callback stub passed to openGadget, and the client returns the selected accounts immediately, maintaining the pipelined flow.

Use Subscription Pattern for Live Updates

Methods that set up push-style updates return a stub representing the subscription; disposing the stub cancels the subscription. This provides a clean, cancellable way to receive live updates for connected accounts or chat messages.

The subscribeConnectedAccounts method returns RpcStub<{}> (line 577 in api.ts). Keep this stub in a ref and dispose it when the component unmounts to cancel the server-side subscription and free associated resources.

Validation and Security Patterns

Annotate Implementations with @validateRpc()

Server-side classes that implement RPC interfaces should be decorated with @validateRpc(). This auto-generates runtime validation that matches TypeScript signatures, eliminating the need for hand-written input checks.

According to the project README and implementations in gatekeeper-google/src/linear.ts (and similar gatekeeper files), this decorator ensures that method arguments conform to expected types before processing, preventing type confusion attacks.

Validate Binding Names to Prevent Prototype Pollution

The helper validateBindingName enforces JavaScript identifier rules and prevents prototype-property collisions. This guarantees that binding maps used as plain objects cannot be poisoned by malicious names like __proto__ or constructor.

Implemented in packages/workshop-shared/src/api.ts (lines 207-220) and utilized in packages/workshop-shared/src/limits.ts, this validation is essential when accepting user-defined binding names that will be used as object keys.

Complete Implementation Example

Putting these patterns together creates a robust, leak-free interaction flow:

// Start OAuth flow and pipeline the token retrieval
const { url, attempt } = await publicApi.startGatekeeperLogin('google');
window.open(url, '_blank');

// Wait for authentication without extra round-trips
const sessionToken = await attempt.wait();

// Open gadget with observer configuration callback
const overseer = await authenticatedApi.openGadget(
  'gadget-abc',
  undefined,
  {
    async configure(needs) {
      // Return selected accounts for missing gatekeepers
      return needs.map(n => ({
        gatekeeperId: n.gatekeeperId,
        accountId: chosenAccountId
      }));
    }
  } as RpcStub<ObserverConfigCallback>
);

// Immediately pipeline a call on the returned stub
await overseer.getChat();

// Subscribe to live account updates
const sub = await authenticatedApi.subscribeConnectedAccounts({
  add(id, desc, vendor, resources, valid) { /* handle add */ },
  remove(id) { /* handle remove */ },
  ready() { /* handle ready */ }
} as RpcStub<ConnectedAccountsSubscriber>);

// Clean up subscriptions and capabilities on unmount
useEffect(() => () => {
  sub[Symbol.dispose]();
  attempt[Symbol.dispose]();
}, []);

Summary

  • Extend RpcTarget on all RPC interfaces to ensure methods are network-reachable and capability-compatible.
  • Return RpcStub<T> from factory methods to enable pipelining and first-class capability passing.
  • Dispose stubs explicitly using [Symbol.dispose]() to prevent Durable Object leaks and resource exhaustion.
  • Wrap stubs in objects before storing in React state to avoid React treating callable stubs as state updaters.
  • Pipeline method calls on returned stubs to eliminate redundant network round-trips and reduce latency.
  • Use @validateRpc() on server implementations to auto-generate runtime type validation.
  • Validate binding names with validateBindingName to prevent prototype pollution attacks.
  • Implement subscription patterns that return disposable stubs for clean, cancellable push updates.

Frequently Asked Questions

What is an RpcStub in Cloudflare OS and why must it be disposed?

An RpcStub is a callable proxy object representing a remote capability in Cloudflare OS. It maintains a reference to a server-side resource, often a Durable Object. You must dispose it using [Symbol.dispose]() to signal the server that the capability is no longer needed, preventing memory leaks and allowing the Durable Object to hibernate or shut down.

How does promise pipelining reduce latency in Cloudflare OS RPC calls?

Promise pipelining allows you to send an RpcStub as a parameter to another RPC call without awaiting its resolution. The server resolves the stub internally before executing the method, enabling chained operations like openGadget().getChat() to complete in a single network round-trip rather than sequential requests. This pattern is implemented in packages/workshop-shared/src/api.ts for methods like openGadget.

Why can't I store RpcStub objects directly in React useState?

React's useState hook treats function arguments specially, invoking them as state updater functions. Because an RpcStub is callable, storing it directly causes React to attempt execution as an updater, throwing an error. The solution, demonstrated in packages/workshop-frontend/src/useWorkspaceOpen.ts, is to wrap the stub in a plain object like { stub: RpcStub<T> } before storing it in state.

What is the purpose of the @validateRpc() decorator?

The @validateRpc() decorator auto-generates runtime validation logic that matches the TypeScript signatures of your RPC interface methods. Applied to server-side implementations (e.g., in gatekeeper files), it removes the need for manual input validation while ensuring type safety and preventing malformed requests from reaching business logic.

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 →