**Status**: Production Ready ✅
/plugin marketplace add secondsky/claude-skills/plugin install cloudflare-durable-objects@claude-skillsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/alarms-api.mdreferences/best-practices.mdreferences/common-patterns.mdreferences/migrations-guide.mdreferences/rpc-patterns.mdreferences/state-api-reference.mdreferences/stubs-routing.mdreferences/top-errors.mdreferences/websocket-hibernation.mdreferences/wrangler-commands.mdscripts/check-versions.shtemplates/alarms-api-do.tstemplates/basic-do.tstemplates/location-hints.tstemplates/multi-do-coordination.tstemplates/package.jsontemplates/rpc-vs-fetch.tstemplates/state-api-patterns.tstemplates/websocket-hibernation-do.tstemplates/wrangler-do-config.jsoncStatus: Production Ready ✅ Last Updated: 2025-11-25 Dependencies: cloudflare-worker-base (recommended) Latest Versions: wrangler@4.50.0+, @cloudflare/workers-types@4.20251125.0+ Official Docs: https://developers.cloudflare.com/durable-objects/
What are Durable Objects? • Quick Start • When to Load References • Class Structure • State API • WebSocket Hibernation • Alarms • RPC vs HTTP • Stubs & Routing • Migrations • Common Patterns • Critical Rules • Known Issues
Globally unique, stateful objects with single-point coordination, strong consistency (ACID), WebSocket Hibernation (thousands of connections), SQLite storage (1GB), and alarms API.
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
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
3. Configure (wrangler.jsonc):
{
"durable_objects": {
"bindings": [{ "name": "COUNTER", "class_name": "Counter" }]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["Counter"] }
]
}
4. Call from Worker (src/index.ts):
import { Counter } from './counter';
interface Env {
COUNTER: DurableObjectNamespace<Counter>;
}
export { Counter };
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const stub = env.COUNTER.getByName('global-counter');
return new Response(`Count: ${await stub.increment()}`);
},
};
Deploy:
bunx wrangler deploy
Load immediately when user mentions:
state-api-reference.md → "storage", "sql", "database", "query", "get/put", "KV", "1GB limit"websocket-hibernation.md → "websocket", "real-time", "chat", "hibernation", "serializeAttachment"alarms-api.md → "alarms", "scheduled tasks", "cron", "periodic", "batch processing"rpc-patterns.md → "RPC", "fetch", "HTTP", "methods", "routing"stubs-routing.md → "stubs", "idFromName", "newUniqueId", "location hints", "jurisdiction"migrations-guide.md → "migrations", "rename", "delete", "transfer", "schema changes"common-patterns.md → "patterns", "examples", "rate limiting", "sessions", "leader election"top-errors.md → errors, "not working", debugging, "binding not found"Load proactively when:
common-patterns.mdtop-errors.md firstwebsocket-hibernation.md before codingstate-api-reference.md for SQL/KV APIsstubs-routing.md for ID methodsAll 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()
Durable Objects provide two storage options:
SQL API (SQLite backend, recommended):
ctx.storage.sqlnew_sqlite_classes in migrationsKey-Value API (available on both backends):
ctx.storage (get/put/delete/list)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.
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:
ctx.acceptWebSocket(server) (enables hibernation)ws.serializeAttachment(data) to persist metadata across hibernationctx.getWebSockets()ws.accept() (standard API, no hibernation)setTimeout/setInterval (prevents hibernation)Handler methods: webSocketMessage(), webSocketClose(), webSocketError()
Quick pattern:
export class ChatRoom extends DurableObject {
sessions: Map<WebSocket, any>;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sessions = new Map();
// Restore connections after hibernation
ctx.getWebSockets().forEach(ws => {
this.sessions.set(ws, ws.deserializeAttachment());
});
}
async fetch(request: Request): Promise<Response> {
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
this.ctx.acceptWebSocket(server); // ← Enables hibernation
server.serializeAttachment({ userId: 'alice' }); // ← Persists across hibernation
this.sessions.set(server, { userId: 'alice' });
return new Response(null, { status: 101, webSocket: client });
}
async webSocketMessage(ws: WebSocket, message: string): Promise<void> {
const session = this.sessions.get(ws);
// Broadcast to all
this.sessions.forEach((_, w) => w.send(message));
}
}
Load references/websocket-hibernation.md for complete handler patterns, hibernation lifecycle, serializeAttachment API, connection management, broadcasting patterns, and hibernation troubleshooting.
Schedule DO to wake up at a future time for batching, cleanup, reminders, or periodic tasks.
Core API:
await ctx.storage.setAlarm(timestamp) - Schedule alarmawait ctx.storage.getAlarm() - Get current alarm time (null if not set)await ctx.storage.deleteAlarm() - Cancel alarmasync alarm(info) - Handler called when alarm firesKey Features:
Quick pattern:
export class Batcher extends DurableObject {
async addItem(item: string): Promise<void> {
await this.ctx.storage.put('items', [...existingItems, item]);
// Schedule batch processing if not already scheduled
if (await this.ctx.storage.getAlarm() === null) {
await this.ctx.storage.setAlarm(Date.now() + 10000); // 10 seconds
}
}
async alarm(info: { retryCount: number; isRetry: boolean }): Promise<void> {
const items = await this.ctx.storage.get('items');
await this.processBatch(items); // Send to API, write to DB, etc.
await this.ctx.storage.put('items', []); // Clear buffer
}
}
Load references/alarms-api.md for periodic alarms pattern, retry handling, error scenarios, cleanup jobs, and batching strategies.
RPC (Recommended): Call DO methods directly like await stub.increment(). Type-safe, simple, auto-serialization. Requires compatibility_date >= 2024-04-03.
HTTP Fetch: Traditional HTTP request/response with async fetch(request) handler. Required for WebSocket upgrades.
Quick comparison:
// RPC Pattern (simpler)
export class Counter extends DurableObject {
async increment(): Promise<number> { // ← Direct method
let value = await this.ctx.storage.get<number>('count') || 0;
return ++value;
}
}
const count = await stub.increment(); // ← Direct call
// HTTP Fetch Pattern
export class Counter extends DurableObject {
async fetch(request: Request): Promise<Response> { // ← HTTP handler
const url = new URL(request.url);
if (url.pathname === '/increment') { /* ... */ }
}
}
const response = await stub.fetch('/increment', { method: 'POST' });
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.
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 globallynewUniqueId() - Random IDs: New unique instance, must store ID for future accessidFromString(idString) - Recreate from saved ID stringGetting 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:
locationHint option when creating stub: { locationHint: 'enam' }Data residency with jurisdiction restrictions:
newUniqueId({ jurisdiction: 'eu' }) or { jurisdiction: 'fedramp' }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 are REQUIRED when creating, renaming, deleting, or transferring DO classes between Workers.
Four migration types:
new_sqlite_classes (recommended, 1GB) or new_classes (legacy KV, 128MB)renamed_classes with from/to mapping (data preserved, bindings forward)deleted_classes (⚠️ immediate deletion, cannot undo, all storage lost)transferred_classes with from_script (moves instances to new Worker)Quick example - Create new DO with SQLite:
{
"durable_objects": {
"bindings": [{ "name": "COUNTER", "class_name": "Counter" }]
},
"migrations": [
{
"tag": "v1", // Unique identifier (append-only)
"new_sqlite_classes": ["Counter"]
}
]
}
CRITICAL rules:
Load references/migrations-guide.md for complete migration patterns, rename/delete/transfer procedures, rollback strategies, and migration gotchas.
Four production-ready patterns for Cloudflare Durable Objects:
Quick example - Rate limiter:
export class RateLimiter extends DurableObject {
async checkLimit(userId: string, limit: number, window: number): Promise<boolean> {
const requests = await this.ctx.storage.get<number[]>(`rate:${userId}`) || [];
const validRequests = requests.filter(t => Date.now() - t < window);
if (validRequests.length >= limit) return false;
validRequests.push(Date.now());
await this.ctx.storage.put(`rate:${userId}`, validRequests);
return true;
}
}
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.
✅ Always:
export default MyDOsuper(ctx, env) first in constructornew_sqlite_classes in migrations (1GB vs 128MB KV)ctx.acceptWebSocket() for hibernation (not ws.accept())sql.exec('... WHERE id = ?', id)blockConcurrencyWhile()❌ Never:
This skill prevents 15+ documented issues. Top 3 most critical:
Error: "binding not found" | Why: DO class not exported
Fix: export default MyDO;
Error: "migrations required" | Why: Created DO without migration entry
Fix: Add { "tag": "v1", "new_sqlite_classes": ["MyDO"] } to migrations
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.
wrangler.jsonc structure:
{
"durable_objects": {
"bindings": [
{ "name": "COUNTER", "class_name": "Counter" } // Binding name must match code
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["Counter"] } // Required for new DOs
]
}
TypeScript types:
import { DurableObject, DurableObjectState, DurableObjectNamespace } from 'cloudflare:workers';
interface Env {
MY_DO: DurableObjectNamespace<MyDurableObject>;
}
export class MyDurableObject extends DurableObject<Env> {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
}
}
Official Docs: https://developers.cloudflare.com/durable-objects/
references/top-errors.md for common problems or check templates/ for working examples