Claude-skill-registry cloudflare-durable-objects
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/cloudflare-durable-objects" ~/.claude/skills/majiayu000-claude-skill-registry-cloudflare-durable-objects-dcbad8 && rm -rf "$T"
skills/data/cloudflare-durable-objects/SKILL.mdCloudflare Durable Objects
Status: Production Ready ✅ Last Updated: 2026-01-21 Dependencies: cloudflare-worker-base (recommended) Latest Versions: wrangler@4.58.0, @cloudflare/workers-types@4.20260109.0 Official Docs: https://developers.cloudflare.com/durable-objects/
Recent Updates (2025):
- Oct 2025: WebSocket message size 1 MiB → 32 MiB, Data Studio UI for SQLite DOs (view/edit storage in dashboard)
- Aug 2025:
API shortcut for named DOsgetByName() - June 2025: @cloudflare/actors library (beta) - recommended SDK with migrations, alarms, Actor class pattern. Note: Beta stability - see active issues before production use (RPC serialization, vitest integration, memory management)
- May 2025: Python Workers support for Durable Objects
- April 2025: SQLite GA with 10GB storage (beta → GA, 1GB → 10GB), Free tier access
- Feb 2025: PRAGMA optimize support, improved error diagnostics with reference IDs
Quick Start
Scaffold new DO project:
npm create cloudflare@latest my-durable-app -- --template=cloudflare/durable-objects-template --ts
Or add to existing Worker:
// src/counter.ts - Durable Object class import { DurableObject } from 'cloudflare:workers'; export class Counter extends DurableObject { async increment(): Promise<number> { let value = (await this.ctx.storage.get<number>('value')) || 0; await this.ctx.storage.put('value', ++value); return value; } } export default Counter; // CRITICAL: Export required
// wrangler.jsonc - Configuration { "durable_objects": { "bindings": [{ "name": "COUNTER", "class_name": "Counter" }] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["Counter"] } // SQLite backend (10GB limit) ] }
// src/index.ts - Worker import { Counter } from './counter'; export { Counter }; export default { async fetch(request: Request, env: { COUNTER: DurableObjectNamespace<Counter> }) { const stub = env.COUNTER.getByName('global-counter'); // Aug 2025: getByName() shortcut return new Response(`Count: ${await stub.increment()}`); } };
DO Class Essentials
import { DurableObject } from 'cloudflare:workers'; export class MyDO extends DurableObject { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); // REQUIRED first line // Load state before requests (optional) ctx.blockConcurrencyWhile(async () => { this.value = await ctx.storage.get('key') || defaultValue; }); } // RPC methods (recommended) async myMethod(): Promise<string> { return 'Hello'; } // HTTP fetch handler (optional) async fetch(request: Request): Promise<Response> { return new Response('OK'); } } export default MyDO; // CRITICAL: Export required // Worker must export DO class too import { MyDO } from './my-do'; export { MyDO };
Constructor Rules:
- ✅ Call
firstsuper(ctx, env) - ✅ Keep minimal - heavy work blocks hibernation wake
- ✅ Use
for storage initializationctx.blockConcurrencyWhile() - ❌ Never
/setTimeout
(use alarms)setInterval - ❌ Don't rely on in-memory state with WebSockets (persist to storage)
Storage API
Two backends available:
- SQLite (recommended): 10GB storage, SQL queries, atomic operations, PITR
- KV: 128MB storage, key-value only
Enable SQLite in migrations:
{ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }] }
SQL API (SQLite backend)
export class MyDO 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 messages (id INTEGER PRIMARY KEY, text TEXT, created_at INTEGER); CREATE INDEX IF NOT EXISTS idx_created ON messages(created_at); PRAGMA optimize; // Feb 2025: Query performance optimization `); } async addMessage(text: string): Promise<number> { const cursor = this.sql.exec('INSERT INTO messages (text, created_at) VALUES (?, ?) RETURNING id', text, Date.now()); return cursor.one<{ id: number }>().id; } async getMessages(limit = 50): Promise<any[]> { return this.sql.exec('SELECT * FROM messages ORDER BY created_at DESC LIMIT ?', limit).toArray(); } }
SQL Methods:
→ cursorsql.exec(query, ...params)
→ single row (throws if none)cursor.one<T>()
→ row or nullcursor.one<T>({ allowNone: true })
→ all rowscursor.toArray<T>()
→ atomic multi-statementctx.storage.transactionSync(() => { ... })
Best Practices:
- ✅ Use
placeholders for parameterized queries? - ✅ Create indexes on frequently queried columns
- ✅ Use
after schema changesPRAGMA optimize - ✅ Add
keyword to table definitions to enforce type affinity and catch type mismatches earlySTRICT - ✅ Convert booleans to integers (0/1) - booleans bind as strings "true"/"false" in SQLite backend
Key-Value API (both backends)
// Single operations await this.ctx.storage.put('key', value); const value = await this.ctx.storage.get<T>('key'); await this.ctx.storage.delete('key'); // Batch operations await this.ctx.storage.put({ key1: val1, key2: val2 }); const map = await this.ctx.storage.get(['key1', 'key2']); await this.ctx.storage.delete(['key1', 'key2']); // List and delete all const map = await this.ctx.storage.list({ prefix: 'user:', limit: 100 }); await this.ctx.storage.deleteAll(); // Atomic on SQLite only // Transactions await this.ctx.storage.transaction(async (txn) => { await txn.put('key1', val1); await txn.put('key2', val2); });
Storage Limits: SQLite 10GB (April 2025 GA) | KV 128MB
WebSocket Hibernation API
Capabilities:
- Thousands of WebSocket connections per instance
- Hibernate when idle (~10s no activity) to save costs
- Auto wake-up when messages arrive
- Message size limit: 32 MiB (Oct 2025, up from 1 MiB)
How it works:
- Active → handles messages
- Idle → ~10s no activity
- Hibernation → in-memory state cleared, WebSockets stay connected
- Wake → message arrives → constructor runs → handler called
CRITICAL: In-memory state is lost on hibernation. Use
serializeAttachment() to persist per-WebSocket metadata.
Hibernation-Safe Pattern
export class ChatRoom extends DurableObject { sessions: Map<WebSocket, { userId: string; username: string }>; constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); this.sessions = new Map(); // CRITICAL: Restore WebSocket metadata 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); const url = new URL(request.url); const metadata = { userId: url.searchParams.get('userId'), username: url.searchParams.get('username') }; // CRITICAL: Use ctx.acceptWebSocket(), NOT ws.accept() this.ctx.acceptWebSocket(server); server.serializeAttachment(metadata); // Persist across hibernation this.sessions.set(server, metadata); return new Response(null, { status: 101, webSocket: client }); } async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> { const session = this.sessions.get(ws); // Handle message (max 32 MiB since Oct 2025) } async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean): Promise<void> { this.sessions.delete(ws); ws.close(code, 'Closing'); } async webSocketError(ws: WebSocket, error: any): Promise<void> { this.sessions.delete(ws); } }
Hibernation Rules:
- ✅
- enables hibernationctx.acceptWebSocket(ws) - ✅
- persist metadataws.serializeAttachment(data) - ✅
- restore in constructorctx.getWebSockets().forEach() - ✅ Use alarms instead of
/setTimeoutsetInterval - ❌
- standard API, no hibernationws.accept() - ❌
/setTimeout
- prevents hibernationsetInterval - ❌ In-progress
- blocks hibernationfetch()
Alarms API
Schedule DO to wake at future time. Use for: batching, cleanup, reminders, periodic tasks.
export class Batcher extends DurableObject { async addItem(item: string): Promise<void> { // Add to buffer const buffer = await this.ctx.storage.get<string[]>('buffer') || []; buffer.push(item); await this.ctx.storage.put('buffer', buffer); // Schedule alarm if not set 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> { if (info.retryCount > 3) return; // Give up after 3 retries const buffer = await this.ctx.storage.get<string[]>('buffer') || []; await this.processBatch(buffer); await this.ctx.storage.put('buffer', []); // Alarm auto-deleted after success } }
API Methods:
- set alarm (overwrites existing)await ctx.storage.setAlarm(Date.now() + 60000)
- get timestamp or nullawait ctx.storage.getAlarm()
- cancel alarmawait ctx.storage.deleteAlarm()
- handler called when alarm firesasync alarm(info)
Behavior:
- ✅ At-least-once execution, auto-retries (up to 6x, exponential backoff)
- ✅ Survives hibernation/eviction
- ✅ Auto-deleted after success
- ⚠️ One alarm per DO (new alarm overwrites)
RPC vs HTTP Fetch
RPC (Recommended): Direct method calls, type-safe, simple
// DO class export class Counter extends DurableObject { async increment(): Promise<number> { let value = (await this.ctx.storage.get<number>('count')) || 0; await this.ctx.storage.put('count', ++value); return value; } } // Worker calls const stub = env.COUNTER.getByName('my-counter'); const count = await stub.increment(); // Type-safe!
HTTP Fetch: Request/response pattern, required for WebSocket upgrades
// DO class export class Counter extends DurableObject { async fetch(request: Request): Promise<Response> { const url = new URL(request.url); if (url.pathname === '/increment') { let value = (await this.ctx.storage.get<number>('count')) || 0; await this.ctx.storage.put('count', ++value); return new Response(JSON.stringify({ count: value })); } return new Response('Not found', { status: 404 }); } } // Worker calls const stub = env.COUNTER.getByName('my-counter'); const response = await stub.fetch('https://fake-host/increment', { method: 'POST' }); const data = await response.json();
When to use: RPC for new projects (simpler), HTTP Fetch for WebSocket upgrades or complex routing
Getting DO Stubs
Three ways to get IDs:
- Consistent routing (same name = same DO)idFromName(name)
const stub = env.CHAT_ROOM.getByName('room-123'); // Aug 2025: Shortcut for idFromName + get // Use for: chat rooms, user sessions, per-tenant logic, singletons
- Random unique ID (must store for reuse)newUniqueId()
const id = env.MY_DO.newUniqueId({ jurisdiction: 'eu' }); // Optional: EU compliance const idString = id.toString(); // Save to KV/D1 for later
- Recreate from saved IDidFromString(idString)
const id = env.MY_DO.idFromString(await env.KV.get('session:123')); const stub = env.MY_DO.get(id);
Location hints (best-effort):
const stub = env.MY_DO.get(id, { locationHint: 'enam' }); // wnam, enam, sam, weur, eeur, apac, oc, afr, me
Jurisdiction (strict enforcement):
const id = env.MY_DO.newUniqueId({ jurisdiction: 'eu' }); // Options: 'eu', 'fedramp' // Cannot combine with location hints, higher latency outside jurisdiction
Migrations
Required for: create, rename, delete, transfer DO classes
1. Create:
{ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Counter"] }] } // SQLite 10GB // Or: "new_classes": ["Counter"] // KV 128MB (legacy)
2. Rename:
{ "migrations": [ { "tag": "v1", "new_sqlite_classes": ["OldName"] }, { "tag": "v2", "renamed_classes": [{ "from": "OldName", "to": "NewName" }] } ]}
3. Delete:
{ "migrations": [ { "tag": "v1", "new_sqlite_classes": ["Counter"] }, { "tag": "v2", "deleted_classes": ["Counter"] } // Immediate deletion, cannot undo ]}
4. Transfer:
{ "migrations": [{ "tag": "v1", "transferred_classes": [ { "from": "OldClass", "from_script": "old-worker", "to": "NewClass" } ]}]}
Migration Rules:
- ❌ Atomic (all instances migrate at once, no gradual rollout)
- ❌ Tags are unique and append-only
- ❌ Cannot enable SQLite on existing KV-backed DOs
- ✅ Code changes don't need migrations (only schema changes)
- ✅ Class names globally unique per account
Common Patterns
Rate Limiting:
async checkLimit(userId: string, limit: number, window: number): Promise<boolean> { const requests = (await this.ctx.storage.get<number[]>(`rate:${userId}`)) || []; const valid = requests.filter(t => Date.now() - t < window); if (valid.length >= limit) return false; valid.push(Date.now()); await this.ctx.storage.put(`rate:${userId}`, valid); return true; }
Session Management with TTL:
async set(key: string, value: any, ttl?: number): Promise<void> { const expiresAt = ttl ? Date.now() + ttl : null; this.sql.exec('INSERT OR REPLACE INTO session (key, value, expires_at) VALUES (?, ?, ?)', key, JSON.stringify(value), expiresAt); } async alarm(): Promise<void> { this.sql.exec('DELETE FROM session WHERE expires_at < ?', Date.now()); await this.ctx.storage.setAlarm(Date.now() + 3600000); // Hourly cleanup }
Leader Election:
async electLeader(workerId: string): Promise<boolean> { try { this.sql.exec('INSERT INTO leader (id, worker_id, elected_at) VALUES (1, ?, ?)', workerId, Date.now()); return true; } catch { return false; } // Already has leader }
Multi-DO Coordination:
// Coordinator delegates to child DOs const gameRoom = env.GAME_ROOM.getByName(gameId); await gameRoom.initialize(); await this.ctx.storage.put(`game:${gameId}`, { created: Date.now() });
Critical Rules
Always Do
✅ Export DO class from Worker
export class MyDO extends DurableObject { } export default MyDO; // Required
✅ Call
in constructorsuper(ctx, env)
constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); // Required first line }
✅ Use
for new DOsnew_sqlite_classes
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }
✅ Use
for hibernationctx.acceptWebSocket()
this.ctx.acceptWebSocket(server); // Enables hibernation
✅ Persist critical state to storage (not just memory)
await this.ctx.storage.put('important', value);
✅ Use alarms instead of setTimeout/setInterval
await this.ctx.storage.setAlarm(Date.now() + 60000);
✅ Use parameterized SQL queries
this.sql.exec('SELECT * FROM table WHERE id = ?', id);
✅ Minimize constructor work
constructor(ctx, env) { super(ctx, env); // Minimal initialization only ctx.blockConcurrencyWhile(async () => { // Load from storage }); }
Never Do
❌ Create DO without migration
// Missing migrations array = error
❌ Forget to export DO class
class MyDO extends DurableObject { } // Missing: export default MyDO;
❌ Use
or setTimeoutsetInterval
setTimeout(() => {}, 1000); // Prevents hibernation
❌ Rely only on in-memory state with WebSockets
// ❌ WRONG: this.sessions will be lost on hibernation // ✅ CORRECT: Use serializeAttachment()
❌ Deploy migrations gradually
# Migrations are atomic - cannot use gradual rollout
❌ Enable SQLite on existing KV-backed DO
// Not supported - must create new DO class instead
❌ Use standard WebSocket API expecting hibernation
ws.accept(); // ❌ No hibernation this.ctx.acceptWebSocket(ws); // ✅ Hibernation enabled
❌ Assume location hints are guaranteed
// Location hints are best-effort only
Known Issues Prevention
This skill prevents 20 documented issues:
Issue #1: Class Not Exported
Error:
"binding not found" or "Class X not found"
Source: https://developers.cloudflare.com/durable-objects/get-started/
Why It Happens: DO class not exported from Worker
Prevention:
export class MyDO extends DurableObject { } export default MyDO; // ← Required
Issue #2: Missing Migration
Error:
"migrations required" or "no migration found for class"
Source: https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/
Why It Happens: Created DO class without migration entry
Prevention: Always add migration when creating new DO class
{ "migrations": [ { "tag": "v1", "new_sqlite_classes": ["MyDO"] } ] }
Issue #3: Wrong Migration Type (KV vs SQLite)
Error: Schema errors, storage API mismatch Source: https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/ Why It Happens: Used
new_classes instead of new_sqlite_classes
Prevention: Use new_sqlite_classes for SQLite backend (recommended)
Issue #4: Constructor Overhead Blocks Hibernation Wake
Error: Slow hibernation wake-up times Source: https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage/ Why It Happens: Heavy work in constructor Prevention: Minimize constructor, use
blockConcurrencyWhile()
constructor(ctx, env) { super(ctx, env); ctx.blockConcurrencyWhile(async () => { // Load from storage }); }
Issue #5: setTimeout Breaks Hibernation
Error: DO never hibernates, high duration charges Source: https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/ Why It Happens:
setTimeout/setInterval prevents hibernation
Prevention: Use alarms API instead
// ❌ WRONG setTimeout(() => {}, 1000); // ✅ CORRECT await this.ctx.storage.setAlarm(Date.now() + 1000);
Issue #6: In-Memory State Lost on Hibernation
Error: WebSocket metadata lost, state reset unexpectedly Source: https://developers.cloudflare.com/durable-objects/best-practices/websockets/ Why It Happens: Relied on in-memory state that's cleared on hibernation Prevention: Use
serializeAttachment() for WebSocket metadata
ws.serializeAttachment({ userId, username }); // Restore in constructor ctx.getWebSockets().forEach(ws => { const metadata = ws.deserializeAttachment(); this.sessions.set(ws, metadata); });
Issue #7: Outgoing WebSocket Cannot Hibernate
Error: High charges despite hibernation API Source: Cloudflare Docs | GitHub Issue #4864 Why It Happens: Durable Objects maintaining persistent connections to external WebSocket services using
new WebSocket('url') cannot hibernate and remain pinned in memory indefinitely
Use Cases Affected:
- Real-time database subscriptions (Supabase, Firebase)
- Message brokers (Redis Streams, Apache Kafka)
- WebSocket connections to external real-time services
- Inter-service communication
Prevention: Only use hibernation for server-side (incoming) WebSockets via
. Outgoing WebSocket connections created withctx.acceptWebSocket()
prevent hibernation. Redesign architecture to avoid outgoing WebSocket connections from Durable Objects if hibernation is required.new WebSocket(url)
Issue #8: Global Uniqueness Confusion
Error: Unexpected DO class name conflicts Source: https://developers.cloudflare.com/durable-objects/platform/known-issues/#global-uniqueness Why It Happens: DO class names are globally unique per account Prevention: Understand DO class names are shared across all Workers in account
Issue #9: deleteAll Issues
Error: Storage not fully deleted, billing continues; or internal error in alarm handler Source: KV Storage API | GitHub Issue #2993 Why It Happens:
- KV backend
can fail partially (not atomic)deleteAll() - SQLite: calling
in alarm handler causes internal error and retry loop (fixed in runtime) Prevention:deleteAll() - Use SQLite backend for atomic deleteAll
- In alarm handlers, call
BEFOREdeleteAlarm()
:deleteAll()
async alarm(info: { retryCount: number }): Promise<void> { await this.ctx.storage.deleteAlarm(); // ← Call first await this.ctx.storage.deleteAll(); // Then delete all }
Issue #10: Binding Name Mismatch
Error: Runtime error accessing DO binding Source: https://developers.cloudflare.com/durable-objects/get-started/ Why It Happens: Binding name in wrangler.jsonc doesn't match code Prevention: Ensure consistency
{ "bindings": [{ "name": "MY_DO", "class_name": "MyDO" }] }
env.MY_DO.getByName('instance'); // Must match binding name
Issue #11: State Size Exceeded
Error:
"state limit exceeded" or storage errors
Source: https://developers.cloudflare.com/durable-objects/platform/pricing/
Why It Happens: Exceeded 1GB (SQLite) or 128MB (KV) limit
Prevention: Monitor storage size, implement cleanup with alarms
Issue #12: Migration Not Atomic
Error: Gradual deployment blocked Source: https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/ Why It Happens: Tried to use gradual rollout with migrations Prevention: Migrations deploy atomically across all instances
Issue #13: Location Hint Ignored
Error: DO created in wrong region Source: https://developers.cloudflare.com/durable-objects/reference/data-location/ Why It Happens: Location hints are best-effort, not guaranteed Prevention: Use jurisdiction for strict requirements
Issue #14: Alarm Retry Failures
Error: Tasks lost after alarm failures Source: https://developers.cloudflare.com/durable-objects/api/alarms/ Why It Happens: Alarm handler throws errors repeatedly Prevention: Implement idempotent alarm handlers
async alarm(info: { retryCount: number }): Promise<void> { if (info.retryCount > 3) { console.error('Giving up after 3 retries'); return; } // Idempotent operation }
Issue #15: Fetch Blocks Hibernation
Error: DO never hibernates despite using hibernation API Source: https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/ Why It Happens: In-progress
fetch() requests prevent hibernation
Prevention: Ensure all async I/O completes before idle period
Issue #16: Boolean Values Bind as Strings in SQLite
Error: Boolean columns contain strings
"true"/"false" instead of integers 0/1; SQL queries with boolean comparisons fail
Source: GitHub Issue #9964
Why It Happens: JavaScript boolean values are serialized as strings in Durable Objects SQLite (inconsistent with D1 behavior)
Prevention: Manually convert booleans to integers and use STRICT tables
// Convert booleans to integers this.sql.exec('INSERT INTO test (bool_col) VALUES (?)', value ? 1 : 0); // Use STRICT tables to catch type mismatches early this.sql.exec(` CREATE TABLE IF NOT EXISTS test ( id INTEGER PRIMARY KEY, bool_col INTEGER NOT NULL ) STRICT; `);
Issue #17: RPC ReadableStream Cancel Logs False Network Errors
Error: Wrangler dev logs show "Network connection lost" when canceling ReadableStream from RPC, despite correct cancellation Source: GitHub Issue #11071 Why It Happens: Canceling ReadableStream returned from Durable Object via RPC triggers misleading error logs in Wrangler dev (presentation issue, not runtime bug) Prevention: No workaround available. The cancellation works correctly - ignore the false error logs in Wrangler dev. Issue does not appear in production or workerd-only setup.
Issue #18: blockConcurrencyWhile Does Not Block in Local Dev (Fixed)
Error: Constructor's
blockConcurrencyWhile doesn't block requests in local dev, causing race conditions hidden during development
Source: GitHub Issue #8686
Why It Happens: Bug in older @cloudflare/vite-plugin and wrangler versions
Prevention: Upgrade to @cloudflare/vite-plugin v1.3.1+ and wrangler v4.18.0+ where this is fixed
Issue #19: RPC Between Multiple wrangler dev Sessions Not Supported
Error:
"Cannot access MyDurableObject#myMethod as Durable Object RPC is not yet supported between multiple wrangler dev sessions"
Source: GitHub Issue #11944
Why It Happens: Accessing a Durable Object over RPC from multiple wrangler dev instances (e.g., separate Workers in monorepo) is not yet supported in local dev
Prevention: Use wrangler dev -c config1 -c config2 to run multiple workers in single session, or use HTTP fetch instead of RPC for cross-worker DO communication during local development
Issue #20: state.id.name Undefined in Constructor (vitest Regression)
Error:
DurableObjectState.id.name is undefined in constructor when using @cloudflare/vitest-pool-workers 0.8.71
Source: GitHub Issue #11580
Why It Happens: Regression in vitest-pool-workers 0.8.71 (worked in 0.8.38)
Prevention: Downgrade to @cloudflare/vitest-pool-workers@0.8.38 or upgrade to later version where this is fixed
Configuration & Types
wrangler.jsonc:
{ "compatibility_date": "2025-11-23", "durable_objects": { "bindings": [{ "name": "COUNTER", "class_name": "Counter" }] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["Counter"] }, { "tag": "v2", "renamed_classes": [{ "from": "Counter", "to": "CounterV2" }] } ] }
TypeScript:
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); this.sql = ctx.storage.sql; } }
Official Documentation
- Durable Objects: https://developers.cloudflare.com/durable-objects/
- State API (SQL): https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
- WebSocket Hibernation: https://developers.cloudflare.com/durable-objects/best-practices/websockets/
- Alarms API: https://developers.cloudflare.com/durable-objects/api/alarms/
- Migrations: https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/
- Best Practices: https://developers.cloudflare.com/durable-objects/best-practices/
- Pricing: https://developers.cloudflare.com/durable-objects/platform/pricing/
Questions? Issues?
- Check
for common problemsreferences/top-errors.md - Review
for working examplestemplates/ - Consult official docs: https://developers.cloudflare.com/durable-objects/
- Verify migrations configuration carefully
Last verified: 2026-01-21 | Skill version: 3.1.0 | Changes: Added 5 new issues (boolean binding, RPC stream cancel, blockConcurrencyWhile local dev, RPC multi-session, vitest regression), expanded Issue #7 (outgoing WebSocket use cases) and Issue #9 (deleteAll alarm interaction), added STRICT tables best practice, updated @cloudflare/actors beta warning