Durable Objects
Build stateful, coordinated applications on Cloudflare's edge using Durable Objects.
When to Use
Need Example
Coordination Chat rooms, multiplayer games, collaborative docs
Strong consistency Inventory, booking systems, turn-based games
Per-entity storage Multi-tenant SaaS, per-user data
Persistent connections WebSockets, real-time notifications
Scheduled work per entity Subscription renewals, game timeouts
Do NOT Use For
-
Stateless request handling (use plain Workers)
-
Maximum global distribution needs
-
High fan-out independent requests
Core Principles
-
Model around coordination atoms - One DO per chat room/game/user, not one global DO
-
Use getByName() for deterministic routing - Same input = same DO instance
-
Use SQLite storage - Configure new_sqlite_classes in migrations
-
Initialize in constructor - Use blockConcurrencyWhile() for schema setup only
-
Use RPC methods - Not fetch() handler (compatibility date >= 2024-04-03)
-
Persist first, cache second - Always write to storage before updating in-memory state
-
One alarm per DO - setAlarm() replaces any existing alarm
Anti-Patterns (NEVER)
-
Single global DO handling all requests (bottleneck)
-
Using blockConcurrencyWhile() on every request (kills throughput)
-
Storing critical state only in memory (lost on eviction/crash)
-
Using await between related storage writes (breaks atomicity)
-
Holding blockConcurrencyWhile() across fetch() or external I/O
Wrangler Configuration
// wrangler.jsonc { "durable_objects": { "bindings": [{ "name": "MY_DO", "class_name": "MyDurableObject" }] }, "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyDurableObject"] }] }
Basic Durable Object Pattern
import { DurableObject } from "cloudflare:workers";
export interface Env { MY_DO: DurableObjectNamespace<MyDurableObject>; }
export class MyDurableObject extends DurableObject<Env> {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
ctx.blockConcurrencyWhile(async () => {
this.ctx.storage.sql.exec( CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, data TEXT NOT NULL ) );
});
}
async addItem(data: string): Promise<number> { const result = this.ctx.storage.sql.exec<{ id: number }>( "INSERT INTO items (data) VALUES (?) RETURNING id", data ); return result.one().id; } }
export default { async fetch(request: Request, env: Env): Promise<Response> { const stub = env.MY_DO.getByName("my-instance"); const id = await stub.addItem("hello"); return Response.json({ id }); }, };
Stub Creation
// Deterministic - preferred for most cases const stub = env.MY_DO.getByName("room-123");
// From existing ID string const id = env.MY_DO.idFromString(storedIdString); const stub = env.MY_DO.get(id);
// New unique ID - store mapping externally const id = env.MY_DO.newUniqueId(); const stub = env.MY_DO.get(id);
Storage Operations
// SQL (synchronous, recommended) this.ctx.storage.sql.exec("INSERT INTO t (c) VALUES (?)", value); const rows = this.ctx.storage.sql.exec<Row>("SELECT * FROM t").toArray();
// KV (async) await this.ctx.storage.put("key", value); const val = await this.ctx.storage.get<Type>("key");
Alarms
// Schedule (replaces existing) await this.ctx.storage.setAlarm(Date.now() + 60_000);
// Handler async alarm(): Promise<void> { // Process scheduled work // Optionally reschedule: await this.ctx.storage.setAlarm(...) }
// Cancel await this.ctx.storage.deleteAlarm();
WebSocket Handler
export class ChatRoom extends DurableObject<Env> { async webSocketMessage(ws: WebSocket, message: string) { // Broadcast to all connected clients for (const client of this.ctx.getWebSockets()) { if (client !== ws) { client.send(message); } } }
async webSocketClose(ws: WebSocket) { // Clean up connection state } }
Testing Quick Start
import { env } from "cloudflare:test"; import { describe, it, expect } from "vitest";
describe("MyDO", () => { it("should work", async () => { const stub = env.MY_DO.getByName("test"); const result = await stub.addItem("test"); expect(result).toBe(1); }); });
Sharding Strategies
By Entity ID
// Each user gets their own DO
const stub = env.USER_DO.getByName(user-${userId});
By Partition Key
// Shard by region or tenant
const stub = env.DATA_DO.getByName(${region}-${tenantId});
Parent-Child
// Parent coordinates, children store data const parent = env.COORDINATOR.getByName("main"); const childId = await parent.getShardFor(key); const child = env.DATA_DO.get(childId);
Best Practices
-
Keep DOs small - Model around natural coordination boundaries
-
Use SQL for structured data - SQLite is recommended over KV
-
Initialize schemas in constructor - Use blockConcurrencyWhile() once
-
Persist before response - Don't rely on in-memory state alone
-
Handle DO eviction - State is lost on eviction, storage persists
-
Use alarms for timeouts - Not external cron jobs
-
Test with @cloudflare/vitest-pool-workers
-
Full DO lifecycle testing
Attribution
Based on cloudflare/skills durable-objects skill.