How Cloudflare OS Enables Real-Time Multiplayer for Gadgets Using Durable Objects
Cloudflare OS powers real-time multiplayer in Gadgets by combining Durable Objects for strongly consistent state with Cap'n Web RPC for bidirectional communication, allowing servers to push updates to clients through function stubs.
Cloudflare OS provides the infrastructure for building collaborative web applications through its Gadget framework. By leveraging Cloudflare's stateful serverless primitives and modern RPC patterns, developers can implement Cloudflare OS real-time multiplayer functionality without managing WebSocket connections or consensus algorithms. The architecture centers on Durable Objects that maintain shared game state and Cap'n Web RPC that enables servers to invoke client methods asynchronously.
Architecture Overview: Durable Objects and Cap'n Web RPC
Stateful Serverless Primitives
Every Gadget in Cloudflare OS runs on top of a Durable Object, which provides a single instance that automatically synchronizes across all connected clients. According to the source code in packages/workshop-backend/src/agent.ts, this guarantees that only one instance processes all RPC calls, eliminating race conditions and ensuring the shared state remains consistent.
Bidirectional RPC Communication
The platform exposes Gadget APIs through Cap'n Web RPC, allowing clients to call server methods while also passing function stubs that the server can later invoke. This creates a publish/subscribe pattern where the server can push updates to clients without polling. The implementation relies on RpcTarget subclasses that act as callbacks for state changes.
Server-Side Implementation: Managing Client Subscriptions
The server maintains a registry of connected clients using persistent references to RPC stubs. As documented in packages/workshop-backend/src/agent.ts (lines 19-23), the Durable Object stores a set of subscribed client callbacks and invokes each one when the shared state changes.
When a client connects, it registers a callback stub through the subscribe() method. The server uses dup() to create a long-lived reference and onRpcBroken() to handle disconnections:
// packages/workshop-backend/src/gadget.ts
import { RpcTarget } from "cloudflare:workers";
export class Gadget extends DurableObject {
// Persistent set of client callbacks
private subscribers = new Set<RpcTarget>();
// Called by a client to register its callback stub
async subscribe(callback: RpcTarget) {
const cb = callback.dup(); // keep a long-lived reference
this.subscribers.add(cb);
// Remove the stub if the client disconnects
cb.onRpcBroken(() => this.subscribers.delete(cb));
}
// Example game move from a client
async makeMove(playerId: string, move: any) {
// Update durable-object storage with the new move
const newState = await this.loadState();
// Broadcast the updated state to all clients
for (const cb of this.subscribers) {
// `update` is a method defined on the client stub
await cb.update(newState);
}
}
}
Client-Side Implementation: RpcTarget Stubs
Clients implement real-time updates by creating an RpcTarget subclass that defines methods the server can invoke. The pattern shown in packages/workshop-backend/src/agent.ts (lines 33-47) demonstrates how clients register these stubs using gadget.subscribe().
Because the stub is an RPC reference, the server can call it asynchronously, and the client's UI updates in real time. The client runs inside a sandboxed iframe where the framework automatically forwards RPC calls over postMessage:
// packages/workshop-frontend/src/client.ts
import { RpcTarget } from "cloudflare:workers";
class Callback extends RpcTarget {
// Called by the server whenever the shared state changes
update(state) {
// Refresh UI – e.g. re-render a game board
renderGame(state);
}
// When the iframe unloads, discard the stub so the server can clean up
[Symbol.dispose]() {
gadget.subscribe(this); // re-subscribe on reconnect if needed
}
}
// `gadget` is the top-level RPC stub automatically injected by the platform
gadget.subscribe(new Callback());
// To make a move:
async function placePiece(row: number, col: number) {
await gadget.makeMove("player-1", { row, col });
}
State Persistence and Consistency Guarantees
Durable Objects guarantee strong consistency, but developers must persist state to storage rather than keeping it only in memory. As noted in packages/workshop-backend/src/agent.ts (lines 56-58), all state must be stored in the Durable Object's KV/SQLite storage to survive restarts.
Always use the storage API to ensure data survives migrations or unexpected terminations:
// Inside the DurableObject class
async loadState() {
const raw = await this.storage.get("gameState");
return raw ?? { board: [], turn: "player-1" };
}
async saveState(state) {
await this.storage.put("gameState", state);
}
Real-Time Collaboration Without WebSockets
Cloudflare OS abstracts away WebSocket management entirely. According to the repository's README.md (lines 44-48), the platform explicitly supports real-time multiplayer experiences similar to Google Docs or collaborative whiteboards by handling all networking through the Durable Object and Cap'n Web RPC layer. Developers never need to manage connection lifecycles or message broadcasting logic manually.
Summary
- Cloudflare OS real-time multiplayer relies on Durable Objects for strongly consistent, stateful serverless compute.
- The Cap'n Web RPC protocol enables bidirectional communication where servers push updates to clients via function stubs.
- Clients implement RpcTarget subclasses that servers invoke when shared state changes.
- The subscribe() pattern in
packages/workshop-backend/src/agent.tsmanages client registrations with automatic cleanup viaonRpcBroken(). - All game state must persist to Durable Object storage to survive restarts and ensure consistency across sessions.
Frequently Asked Questions
How does Cloudflare OS handle client disconnections in multiplayer Gadgets?
When a client disconnects, the Durable Object automatically detects the broken RPC connection through the onRpcBroken() callback. As implemented in packages/workshop-backend/src/agent.ts, the server removes the client's stub from the subscription set, preventing failed update attempts to disconnected clients without requiring explicit unsubscribe calls from the client.
What makes Durable Objects suitable for real-time multiplayer games?
Durable Objects provide a single, strongly consistent instance that processes all requests for a specific game session. This eliminates race conditions because only one instance updates the shared state at any time. According to the Cloudflare OS source code, this guarantee extends across all connected clients, making it ideal for turn-based or real-time collaborative applications.
Do developers need to implement WebSocket handling for Gadget multiplayer?
No. Cloudflare OS handles all underlying communication through Cap'n Web RPC, which automatically forwards calls over postMessage within the sandboxed iframe environment. The framework manages connection lifecycles, reconnection logic, and state synchronization, allowing developers to focus on game logic rather than networking code.
How is game state persisted in Cloudflare OS multiplayer Gadgets?
Game state must be explicitly written to the Durable Object's storage using this.storage.put() rather than kept in memory. As documented in packages/workshop-backend/src/agent.ts (lines 56-58), this ensures the state survives process restarts, migrations, or unexpected terminations. Developers should implement loadState() and saveState() methods to serialize and deserialize game data from the persistent store.
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 →