Cloudflare Durable Objects for stateful coordination and real-time apps. Use for chat, multiplayer games, WebSocket hibernation, or encountering class export, migration, alarm errors.
npm create cloudflare@latest my-durable-app -- \
--template=cloudflare/durable-objects-template --ts --git --deploy false
cd my-durable-app && bun install && npm run dev
Option 2: Add to Existing Worker
1. Install types:
bun add -d @cloudflare/workers-types
2. Create DO class (src/counter.ts):
import { DurableObject } from 'cloudflare:workers';
export class Counter extends DurableObject {
async increment(): Promise<number> {
let value: number = (await this.ctx.storage.get('value')) || 0;
await this.ctx.storage.put('value', ++value);
return value;
}
}
export default Counter; // CRITICAL
Creating first DO → Load stubs-routing.md for ID methods
Writing tests → Load vitest-testing.md for testing patterns
Planning deployment → Load gradual-deployments.md for rollout strategy
Migration needed → Load migration-cheatsheet.md for quick reference
Using DO name inside DO → Load rpc-metadata.md for RpcTarget pattern
TypeScript configuration → Load typescript-config.md for setup
Durable Object Class Structure
All DOs extend DurableObject and MUST be exported:
import { DurableObject } from 'cloudflare:workers';
export class MyDO extends DurableObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env); // Required first line
// Keep minimal - heavy work blocks hibernation
ctx.blockConcurrencyWhile(async () => {
// Load from storage before handling requests
});
}
async myMethod(): Promise<string> { // RPC method (recommended)
return 'Hello!';
}
}
export default MyDO; // CRITICAL: Must export
this.ctx provides:storage (SQL/KV), id (unique ID), waitUntil(), acceptWebSocket()
State API - Persistent Storage
Durable Objects provide two storage options:
SQL API (SQLite backend, recommended):
Access via ctx.storage.sql
Up to 1GB storage per instance
SQL queries with transactions, indexes, cursors
Atomic operations (deleteAll is all-or-nothing)
Use new_sqlite_classes in migrations
Key-Value API (available on both backends):
Access via ctx.storage (get/put/delete/list)
Simple key-value operations
Async transactions supported
128MB limit on KV backend, 1GB on SQLite
Quick example:
export class Counter extends DurableObject {
sql: SqlStorage;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
this.sql.exec('CREATE TABLE IF NOT EXISTS counts (key TEXT PRIMARY KEY, value INTEGER)');
}
async increment(): Promise<number> {
this.sql.exec('INSERT OR REPLACE INTO counts (key, value) VALUES (?, ?)', 'count', 1);
return this.sql.exec('SELECT value FROM counts WHERE key = ?', 'count').one<{value: number}>().value;
}
}
Load references/state-api-reference.md for complete SQL and KV API documentation, cursor operations, transactions, parameterized queries, storage limits, and migration patterns.
WebSocket Hibernation API
Handle thousands of WebSocket connections per DO instance with automatic hibernation when idle (~10s no activity), saving duration costs. Connections stay open at the edge while DO sleeps.
CRITICAL Rules:
✅ Use ctx.acceptWebSocket(server) (enables hibernation)
✅ Use ws.serializeAttachment(data) to persist metadata across hibernation
✅ Restore connections in constructor with ctx.getWebSockets()
❌ Don't use ws.accept() (standard API, no hibernation)
❌ Don't use setTimeout/setInterval (prevents hibernation)
Use RPC for: New projects, type safety, simple method calls
Use HTTP Fetch for: WebSocket upgrades, complex routing, legacy code
Load references/rpc-patterns.md for complete RPC vs Fetch comparison, migration guide, error handling patterns, and method visibility control.
Creating Durable Object Stubs and Routing
To interact with a Durable Object from a Worker: get an ID → create a stub → call methods.
Three ID creation methods:
idFromName(name) - Named DOs (most common): Deterministic routing to same instance globally
newUniqueId() - Random IDs: New unique instance, must store ID for future access
idFromString(idString) - Recreate from saved ID string
Getting stubs:
// Method 1: From ID
const id = env.CHAT_ROOM.idFromName('room-123');
const stub = env.CHAT_ROOM.get(id);
// Method 2: Shortcut for named DOs (recommended)
const stub = env.CHAT_ROOM.getByName('room-123');
await stub.myMethod();
Geographic routing with location hints:
Set locationHint option when creating stub: { locationHint: 'enam' }
Best-effort (not guaranteed), only affects first creation
Data residency with jurisdiction restrictions:
Use newUniqueId({ jurisdiction: 'eu' }) or { jurisdiction: 'fedramp' }
Strictly enforced (DO never leaves jurisdiction)
Cannot combine with location hints
Required for GDPR/FedRAMP compliance
Load references/stubs-routing.md for complete guide to ID methods, stub management, location hints, jurisdiction restrictions, use cases, best practices, and error handling patterns.
Migrations - Managing DO Classes
Migrations are REQUIRED when creating, renaming, deleting, or transferring DO classes between Workers.
Four migration types:
Create New DO: Use new_sqlite_classes (recommended, 1GB) or new_classes (legacy KV, 128MB)
Rename DO: Use renamed_classes with from/to mapping (data preserved, bindings forward)
Delete DO: Use deleted_classes (⚠️ immediate deletion, cannot undo, all storage lost)
Transfer DO: Use transferred_classes with from_script (moves instances to new Worker)
Load references/common-patterns.md for complete implementations of all 4 patterns with full code examples, SQL schemas, alarm usage, error handling, and best practices.
Critical Rules
✅ Always:
Export DO class: export default MyDO
Call super(ctx, env) first in constructor
Use new_sqlite_classes in migrations (1GB vs 128MB KV)
Use ctx.acceptWebSocket() for hibernation (not ws.accept())
Persist state to storage (not just memory)
Use alarms instead of setTimeout/setInterval
Use parameterized SQL: sql.exec('... WHERE id = ?', id)
Minimize constructor work, use blockConcurrencyWhile()
❌ Never:
Create DO without migration (error)
Forget to export class (binding not found)
Use setTimeout/setInterval (prevents hibernation)
Rely only on in-memory state for WebSockets (use serializeAttachment)
Deploy migrations gradually (migrations are atomic)
Enable SQLite on existing KV-backed DO (must create new class)
Assume location hints are guaranteed (best-effort only)
Known Issues Prevention
This skill prevents 15+ documented issues. Top 3 most critical:
Issue #1: Class Not Exported
Error: "binding not found" | Why: DO class not exported
Fix: export default MyDO;
Issue #2: Missing Migration
Error: "migrations required" | Why: Created DO without migration entry
Fix: Add { "tag": "v1", "new_sqlite_classes": ["MyDO"] } to migrations
Issue #3: setTimeout Breaks Hibernation
Error: DO never hibernates, high charges | Why: setTimeout prevents hibernation
Fix: Use await ctx.storage.setAlarm(Date.now() + 1000) instead
12 more issues covered: Wrong migration type, constructor overhead, in-memory state lost, outgoing WebSocket no hibernation, global uniqueness confusion, partial deleteAll, binding mismatch, state size exceeded, migration not atomic, location hint ignored, alarm retry failures, fetch blocks hibernation.
Load references/top-errors.md for complete error catalog with all 15+ issues, detailed prevention strategies, debugging steps, and resolution patterns.
Configuration & TypeScript
Configure wrangler.jsonc with DO bindings and migrations, set up TypeScript types with proper exports.
Load references/typescript-config.md for: wrangler.jsonc structure, TypeScript types, Env interface, tsconfig.json, common type issues