cloudflare-durable-objects
About
This Claude Skill provides comprehensive guidance for implementing Cloudflare Durable Objects, which are globally unique stateful objects for real-time applications. It helps developers build WebSocket servers with hibernation, coordinate multi-client systems, and manage persistent state for chat rooms, multiplayer games, and workflows. The skill specifically addresses 15+ common issues including migration errors, hibernation problems, and global uniqueness confusion.
Quick Install
Claude Code
Recommended/plugin add https://github.com/jezweb/claude-skillsgit clone https://github.com/jezweb/claude-skills.git ~/.claude/skills/cloudflare-durable-objectsCopy and paste this command in Claude Code to install this skill
Documentation
Cloudflare Durable Objects
Status: Production Ready ✅ Last Updated: 2025-10-22 Dependencies: cloudflare-worker-base (recommended) Latest Versions: [email protected]+, @cloudflare/[email protected]+ Official Docs: https://developers.cloudflare.com/durable-objects/
What are Durable Objects?
Cloudflare Durable Objects are globally unique, stateful objects that provide:
- Single-point coordination - Each Durable Object instance is globally unique across Cloudflare's network
- Strong consistency - Transactional, serializable storage (ACID guarantees)
- Real-time communication - WebSocket Hibernation API for thousands of connections per instance
- Persistent state - Built-in SQLite database (up to 1GB) or key-value storage
- Scheduled tasks - Alarms API for future task execution
- Global distribution - Automatically routed to optimal location
- Automatic scaling - Millions of independent instances
Use Cases:
- Chat rooms and real-time collaboration
- Multiplayer game servers
- Rate limiting and session management
- Leader election and coordination
- WebSocket servers with hibernation
- Stateful workflows and queues
- Per-user or per-room logic
Quick Start (10 Minutes)
Option 1: Scaffold New DO Project
npm create cloudflare@latest my-durable-app -- \
--template=cloudflare/durable-objects-template \
--ts \
--git \
--deploy false
cd my-durable-app
npm install
npm run dev
What this creates:
- Complete Durable Objects project structure
- TypeScript configuration
- wrangler.jsonc with bindings and migrations
- Example DO class implementation
- Worker to call the DO
Option 2: Add to Existing Worker
cd my-existing-worker
npm install -D @cloudflare/workers-types
Create a Durable Object class (src/counter.ts):
import { DurableObject } from 'cloudflare:workers';
export class Counter extends DurableObject {
async increment(): Promise<number> {
// Get current value from storage (default to 0)
let value: number = (await this.ctx.storage.get('value')) || 0;
// Increment
value += 1;
// Save back to storage
await this.ctx.storage.put('value', value);
return value;
}
async get(): Promise<number> {
return (await this.ctx.storage.get('value')) || 0;
}
}
// CRITICAL: Export the class
export default Counter;
Configure wrangler.jsonc:
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-22",
// Durable Objects binding
"durable_objects": {
"bindings": [
{
"name": "COUNTER", // How you access it: env.COUNTER
"class_name": "Counter" // MUST match exported class name
}
]
},
// REQUIRED: Migration for new DO class
"migrations": [
{
"tag": "v1", // Unique migration identifier
"new_sqlite_classes": [ // Use SQLite backend (recommended)
"Counter"
]
}
]
}
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> {
// Get Durable Object stub by name
const id = env.COUNTER.idFromName('global-counter');
const stub = env.COUNTER.get(id);
// Call RPC method on the DO
const count = await stub.increment();
return new Response(`Count: ${count}`);
},
};
Deploy:
npx wrangler deploy
Durable Object Class Structure
Base Class Pattern
All Durable Objects MUST extend DurableObject from cloudflare:workers:
import { DurableObject } from 'cloudflare:workers';
export class MyDurableObject extends DurableObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// Optional: Initialize from storage
ctx.blockConcurrencyWhile(async () => {
// Load state before handling requests
this.someValue = await ctx.storage.get('someKey') || defaultValue;
});
}
// RPC methods (recommended)
async myMethod(): Promise<string> {
return 'Hello from DO!';
}
// Optional: HTTP fetch handler
async fetch(request: Request): Promise<Response> {
return new Response('Hello from DO fetch!');
}
}
// CRITICAL: Export the class
export default MyDurableObject;
Constructor Pattern
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env); // REQUIRED
// Access to environment bindings
this.env = env;
// this.ctx provides:
// - this.ctx.storage (storage API)
// - this.ctx.id (unique ID)
// - this.ctx.waitUntil() (background tasks)
// - this.ctx.acceptWebSocket() (WebSocket hibernation)
}
CRITICAL Rules:
- ✅ Always call
super(ctx, env)first - ✅ Keep constructor minimal - heavy work blocks hibernation wake-up
- ✅ Use
ctx.blockConcurrencyWhile()to initialize from storage before requests - ❌ Never use
setTimeoutorsetInterval- breaks hibernation (use alarms instead) - ❌ Don't rely only on in-memory state with WebSockets - persist to storage
Exporting the Class
// Export as default (required for Worker to use it)
export default MyDurableObject;
// Also export as named export (for type inference in Worker)
export { MyDurableObject };
In Worker:
// Import the class for types
import { MyDurableObject } from './my-durable-object';
// Export it so Worker can instantiate it
export { MyDurableObject };
interface Env {
MY_DO: DurableObjectNamespace<MyDurableObject>;
}
State API - Persistent Storage
Durable Objects provide two storage APIs depending on the backend:
- SQL API (SQLite backend) - Recommended
- Key-Value API (KV or SQLite backend)
Enable SQLite Backend (Recommended)
In wrangler.jsonc migrations:
{
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MyDurableObject"] // ← Use this for SQLite
}
]
}
Why SQLite?
- ✅ Up to 1GB storage (vs 128MB for KV backend)
- ✅ Atomic operations (deleteAll is all-or-nothing)
- ✅ SQL queries with transactions
- ✅ Point-in-time recovery (PITR)
- ✅ Synchronous KV API available too
SQL API
Access via ctx.storage.sql:
import { DurableObject } from 'cloudflare:workers';
export class MyDurableObject extends DurableObject {
sql: SqlStorage;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
// Create table on first run
this.sql.exec(`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
user TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_created_at ON messages(created_at);
`);
}
async addMessage(text: string, user: string): Promise<number> {
// Insert with exec (returns cursor)
const cursor = this.sql.exec(
'INSERT INTO messages (text, user, created_at) VALUES (?, ?, ?) RETURNING id',
text,
user,
Date.now()
);
const row = cursor.one<{ id: number }>();
return row.id;
}
async getMessages(limit: number = 50): Promise<any[]> {
const cursor = this.sql.exec(
'SELECT * FROM messages ORDER BY created_at DESC LIMIT ?',
limit
);
// Convert cursor to array
return cursor.toArray();
}
async deleteOldMessages(beforeTimestamp: number): Promise<void> {
this.sql.exec(
'DELETE FROM messages WHERE created_at < ?',
beforeTimestamp
);
}
}
SQL API Methods:
// Execute query (returns cursor)
const cursor = this.sql.exec('SELECT * FROM table WHERE id = ?', id);
// Get single row
const row = cursor.one<RowType>();
// Get first row or null
const row = cursor.one<RowType>({ allowNone: true });
// Get all rows as array
const rows = cursor.toArray<RowType>();
// Iterate cursor
for (const row of cursor) {
// Process row
}
// Transactions (synchronous)
this.ctx.storage.transactionSync(() => {
this.sql.exec('INSERT INTO table1 ...');
this.sql.exec('UPDATE table2 ...');
// All or nothing
});
CRITICAL SQL Rules:
- ✅ Always use parameterized queries with
?placeholders - ✅ Create indexes for frequently queried columns
- ✅ Use transactions for multi-statement operations
- ❌ Don't access the hidden
__cf_kvtable (used internally for KV API) - ❌ Don't enable SQLite on existing deployed KV-backed DOs (not supported)
Key-Value API
Available on both SQLite and KV backends via ctx.storage:
import { DurableObject } from 'cloudflare:workers';
export class MyDurableObject extends DurableObject {
async increment(): Promise<number> {
// Get value
let count = await this.ctx.storage.get<number>('count') || 0;
// Increment
count += 1;
// Put value back
await this.ctx.storage.put('count', count);
return count;
}
async batchOperations(): Promise<void> {
// Get multiple keys
const map = await this.ctx.storage.get<number>(['key1', 'key2', 'key3']);
// Put multiple keys
await this.ctx.storage.put({
key1: 'value1',
key2: 'value2',
key3: 'value3',
});
// Delete key
await this.ctx.storage.delete('key1');
// Delete multiple keys
await this.ctx.storage.delete(['key2', 'key3']);
}
async listKeys(): Promise<string[]> {
// List all keys
const map = await this.ctx.storage.list();
return Array.from(map.keys());
// List with prefix
const mapWithPrefix = await this.ctx.storage.list({
prefix: 'user:',
limit: 100,
});
}
async deleteAllStorage(): Promise<void> {
// Delete alarm first (if set)
await this.ctx.storage.deleteAlarm();
// Delete all storage (DO will cease to exist after shutdown)
await this.ctx.storage.deleteAll();
}
}
KV API Methods:
// Get single value
const value = await this.ctx.storage.get<T>('key');
// Get multiple values (returns Map)
const map = await this.ctx.storage.get<T>(['key1', 'key2']);
// Put single value
await this.ctx.storage.put('key', value);
// Put multiple values
await this.ctx.storage.put({ key1: value1, key2: value2 });
// Delete single key
await this.ctx.storage.delete('key');
// Delete multiple keys
await this.ctx.storage.delete(['key1', 'key2']);
// List keys
const map = await this.ctx.storage.list<T>({
prefix: 'user:',
limit: 100,
reverse: false
});
// Delete all (atomic on SQLite, may be partial on KV backend)
await this.ctx.storage.deleteAll();
// Transactions (async)
await this.ctx.storage.transaction(async (txn) => {
await txn.put('key1', value1);
await txn.put('key2', value2);
// All or nothing
});
Storage Limits:
- SQLite backend: Up to 1GB storage per DO instance
- KV backend: Up to 128MB storage per DO instance
WebSocket Hibernation API
The WebSocket Hibernation API allows Durable Objects to:
- Handle thousands of WebSocket connections per instance
- Hibernate when idle (no messages, no events) to save costs
- Wake up automatically when messages arrive
- Maintain connections without incurring duration charges during idle periods
Use for: Chat rooms, real-time collaboration, multiplayer games, live updates
How Hibernation Works
- Active state - DO is in memory, handling messages
- Idle state - No messages for ~10 seconds, DO can hibernate
- Hibernation - In-memory state cleared, WebSockets stay connected to Cloudflare edge
- Wake up - New message arrives → constructor runs → handler method called
CRITICAL: In-memory state is lost on hibernation. Use serializeAttachment() to persist per-WebSocket metadata.
WebSocket Server Pattern
import { DurableObject } from 'cloudflare:workers';
export class ChatRoom extends DurableObject {
sessions: Map<WebSocket, { userId: string; username: string }>;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// Restore WebSocket connections after hibernation
this.sessions = new Map();
ctx.getWebSockets().forEach((ws) => {
// Deserialize attachment (persisted metadata)
const attachment = ws.deserializeAttachment();
this.sessions.set(ws, attachment);
});
}
async fetch(request: Request): Promise<Response> {
// Expect WebSocket upgrade request
const upgradeHeader = request.headers.get('Upgrade');
if (upgradeHeader !== 'websocket') {
return new Response('Expected websocket', { status: 426 });
}
// Create WebSocket pair
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
// Get user info from URL or headers
const url = new URL(request.url);
const userId = url.searchParams.get('userId') || 'anonymous';
const username = url.searchParams.get('username') || 'Anonymous';
// Accept WebSocket with hibernation
// CRITICAL: Use ctx.acceptWebSocket(), NOT ws.accept()
this.ctx.acceptWebSocket(server);
// Serialize metadata to persist across hibernation
const metadata = { userId, username };
server.serializeAttachment(metadata);
// Track in-memory (will be restored after hibernation)
this.sessions.set(server, metadata);
// Notify others
this.broadcast(`${username} joined`, server);
// Return client WebSocket to browser
return new Response(null, {
status: 101,
webSocket: client,
});
}
// Called when WebSocket receives a message
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
const session = this.sessions.get(ws);
if (typeof message === 'string') {
const data = JSON.parse(message);
if (data.type === 'chat') {
// Broadcast to all connections
this.broadcast(`${session?.username}: ${data.text}`, ws);
}
}
}
// Called when WebSocket closes
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean): Promise<void> {
const session = this.sessions.get(ws);
this.sessions.delete(ws);
// Close the WebSocket
ws.close(code, 'Durable Object closing WebSocket');
// Notify others
if (session) {
this.broadcast(`${session.username} left`);
}
}
// Called on WebSocket errors
async webSocketError(ws: WebSocket, error: any): Promise<void> {
console.error('WebSocket error:', error);
const session = this.sessions.get(ws);
this.sessions.delete(ws);
}
// Helper to broadcast to all connections
broadcast(message: string, except?: WebSocket): void {
this.sessions.forEach((session, ws) => {
if (ws !== except && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'message', text: message }));
}
});
}
}
WebSocket Handler Methods:
// Receive message from client
async webSocketMessage(
ws: WebSocket,
message: string | ArrayBuffer
): Promise<void> {
// Handle message
}
// WebSocket closed by client
async webSocketClose(
ws: WebSocket,
code: number,
reason: string,
wasClean: boolean
): Promise<void> {
// Cleanup
}
// WebSocket error occurred
async webSocketError(
ws: WebSocket,
error: any
): Promise<void> {
// Handle error
}
Hibernation-Safe Patterns:
// ✅ CORRECT: Use ctx.acceptWebSocket (enables hibernation)
this.ctx.acceptWebSocket(server);
// ❌ WRONG: Don't use ws.accept() (standard API, no hibernation)
server.accept();
// ✅ CORRECT: Persist metadata across hibernation
server.serializeAttachment({ userId: '123', username: 'Alice' });
// ✅ CORRECT: Restore metadata in constructor
constructor(ctx, env) {
super(ctx, env);
ctx.getWebSockets().forEach((ws) => {
const metadata = ws.deserializeAttachment();
this.sessions.set(ws, metadata);
});
}
// ❌ WRONG: Don't use setTimeout/setInterval (prevents hibernation)
setTimeout(() => { /* ... */ }, 1000); // ❌ NEVER DO THIS
// ✅ CORRECT: Use alarms for scheduled tasks
await this.ctx.storage.setAlarm(Date.now() + 60000);
When Hibernation Does NOT Occur:
setTimeoutorsetIntervalcallbacks are pending- In-progress
fetch()request (awaited I/O) - Standard WebSocket API is used (not hibernation API)
- Request/event is still being processed
Alarms API - Scheduled Tasks
The Alarms API allows Durable Objects to schedule themselves to wake up at a specific time in the future.
Use for: Batching, cleanup jobs, reminders, periodic tasks, delayed operations
Basic Alarm Pattern
import { DurableObject } from 'cloudflare:workers';
export class Batcher extends DurableObject {
buffer: string[];
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
ctx.blockConcurrencyWhile(async () => {
// Restore buffer from storage
this.buffer = await ctx.storage.get('buffer') || [];
});
}
async addItem(item: string): Promise<void> {
this.buffer.push(item);
await this.ctx.storage.put('buffer', this.buffer);
// Schedule alarm for 10 seconds from now (if not already set)
const currentAlarm = await this.ctx.storage.getAlarm();
if (currentAlarm === null) {
await this.ctx.storage.setAlarm(Date.now() + 10000);
}
}
// Called when alarm fires
async alarm(alarmInfo: { retryCount: number; isRetry: boolean }): Promise<void> {
console.log(`Alarm fired (retry count: ${alarmInfo.retryCount})`);
// Process batch
if (this.buffer.length > 0) {
await this.processBatch(this.buffer);
// Clear buffer
this.buffer = [];
await this.ctx.storage.put('buffer', []);
}
// Alarm is automatically deleted after successful execution
}
async processBatch(items: string[]): Promise<void> {
// Send to external API, write to database, etc.
console.log(`Processing ${items.length} items:`, items);
}
}
Alarm API Methods:
// Set alarm to fire at specific timestamp
await this.ctx.storage.setAlarm(Date.now() + 60000); // 60 seconds from now
// Set alarm to fire at specific date
await this.ctx.storage.setAlarm(new Date('2025-12-31T23:59:59Z'));
// Get current alarm (null if not set)
const alarmTime = await this.ctx.storage.getAlarm();
// Delete alarm
await this.ctx.storage.deleteAlarm();
// Alarm handler (called when alarm fires)
async alarm(alarmInfo: { retryCount: number; isRetry: boolean }): Promise<void> {
// Do work
}
Alarm Behavior:
- ✅ Guaranteed at-least-once execution - will retry on failure
- ✅ Automatic retries - up to 6 retries with exponential backoff (starting at 2 seconds)
- ✅ Persistent - survives DO hibernation and eviction
- ✅ Automatically deleted after successful execution
- ⚠️ One alarm per DO - setting a new alarm overwrites the previous one
Retry Pattern (Idempotent Operations):
async alarm(alarmInfo: { retryCount: number; isRetry: boolean }): Promise<void> {
if (alarmInfo.retryCount > 3) {
console.error('Alarm failed after 3 retries, giving up');
return;
}
try {
// Idempotent operation (safe to retry)
await this.sendNotification();
} catch (error) {
console.error('Alarm failed:', error);
throw error; // Will trigger retry
}
}
RPC vs HTTP Fetch
Durable Objects support two invocation patterns:
- RPC (Remote Procedure Call) - Recommended for new projects
- HTTP Fetch - For HTTP request/response flows or legacy compatibility
RPC Pattern (Recommended)
Enable RPC with compatibility date >= 2024-04-03:
{
"compatibility_date": "2025-10-22"
}
Define RPC methods on DO class:
export class Counter extends DurableObject {
// Public RPC methods (automatically exposed)
async increment(): Promise<number> {
let value = await this.ctx.storage.get<number>('count') || 0;
value += 1;
await this.ctx.storage.put('count', value);
return value;
}
async decrement(): Promise<number> {
let value = await this.ctx.storage.get<number>('count') || 0;
value -= 1;
await this.ctx.storage.put('count', value);
return value;
}
async get(): Promise<number> {
return await this.ctx.storage.get<number>('count') || 0;
}
}
Call from Worker:
// Get stub
const id = env.COUNTER.idFromName('my-counter');
const stub = env.COUNTER.get(id);
// Call RPC methods directly
const count = await stub.increment();
const current = await stub.get();
RPC Benefits:
- ✅ Type-safe - TypeScript knows method signatures
- ✅ Simple - Direct method calls, no HTTP ceremony
- ✅ Automatic serialization - Handles structured data
- ✅ Exception propagation - Errors thrown in DO are received in Worker
HTTP Fetch Pattern
Define fetch() handler on DO class:
export class Counter extends DurableObject {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/increment' && request.method === 'POST') {
let value = await this.ctx.storage.get<number>('count') || 0;
value += 1;
await this.ctx.storage.put('count', value);
return new Response(JSON.stringify({ count: value }));
}
if (url.pathname === '/get' && request.method === 'GET') {
let value = await this.ctx.storage.get<number>('count') || 0;
return new Response(JSON.stringify({ count: value }));
}
return new Response('Not found', { status: 404 });
}
}
Call from Worker:
// Get stub
const id = env.COUNTER.idFromName('my-counter');
const stub = env.COUNTER.get(id);
// Call fetch
const response = await stub.fetch('https://fake-host/increment', {
method: 'POST',
});
const data = await response.json();
When to Use Each
| Use Case | Recommendation |
|---|---|
| New project | ✅ RPC (simpler, type-safe) |
| HTTP request/response flow | HTTP Fetch |
| Complex routing logic | HTTP Fetch |
| Type safety important | ✅ RPC |
| Legacy compatibility | HTTP Fetch |
| WebSocket upgrades | HTTP Fetch (required) |
Creating Durable Object Stubs and Routing
To interact with a Durable Object from a Worker, you need to:
- Get a Durable Object ID
- Create a stub from the ID
- Call methods on the stub
Getting Durable Object IDs
Three methods to create IDs:
1. idFromName(name) - Named DOs (Most Common)
Use when you want consistent routing to the same DO instance based on a name:
// Same name always routes to same DO instance globally
const roomId = env.CHAT_ROOM.idFromName('room-123');
const userId = env.USER_SESSION.idFromName('user-alice');
const globalCounter = env.COUNTER.idFromName('global');
Use for:
- Chat rooms (name = room ID)
- User sessions (name = user ID)
- Per-tenant logic (name = tenant ID)
- Global singletons (name = 'global')
Characteristics:
- ✅ Deterministic - same name = same DO instance
- ✅ Easy to reference - just need the name string
- ⚠️ First access latency - ~100-300ms for global uniqueness check
- ⚠️ Cached after first use - subsequent access is fast
2. newUniqueId() - Random IDs
Use when you need a new, unique DO instance:
// Creates a random, globally unique ID
const id = env.MY_DO.newUniqueId();
// With jurisdiction restriction (EU data residency)
const euId = env.MY_DO.newUniqueId({ jurisdiction: 'eu' });
// Store the ID for future use
const idString = id.toString();
await env.KV.put('session:123', idString);
Use for:
- Creating new sessions/rooms that don't exist yet
- One-time use DOs
- When you don't have a natural name
Characteristics:
- ✅ Lower latency on first use (no global uniqueness check)
- ⚠️ Must store ID to access same DO later
- ⚠️ ID format is opaque - can't derive meaning from it
3. idFromString(idString) - Recreate from Saved ID
Use when you've previously stored an ID and need to recreate it:
// Get stored ID string (from KV, D1, cookie, etc.)
const idString = await env.KV.get('session:123');
// Recreate ID
const id = env.MY_DO.idFromString(idString);
// Get stub
const stub = env.MY_DO.get(id);
Throws exception if:
- ID string is invalid
- ID was not created from the same
DurableObjectNamespace
Getting Stubs
Method 1: get(id) - From ID
const id = env.MY_DO.idFromName('my-instance');
const stub = env.MY_DO.get(id);
// Call methods
await stub.myMethod();
Method 2: getByName(name) - Shortcut for Named DOs
// Shortcut that combines idFromName + get
const stub = env.MY_DO.getByName('my-instance');
// Equivalent to:
// const id = env.MY_DO.idFromName('my-instance');
// const stub = env.MY_DO.get(id);
await stub.myMethod();
Recommended for named DOs (cleaner code).
Location Hints (Geographic Routing)
Control WHERE a Durable Object is created with location hints:
// Create DO near specific location
const id = env.MY_DO.idFromName('user-alice');
const stub = env.MY_DO.get(id, { locationHint: 'enam' }); // Eastern North America
// Available location hints:
// - 'wnam' - Western North America
// - 'enam' - Eastern North America
// - 'sam' - South America
// - 'weur' - Western Europe
// - 'eeur' - Eastern Europe
// - 'apac' - Asia-Pacific
// - 'oc' - Oceania
// - 'afr' - Africa
// - 'me' - Middle East
When to use:
- ✅ Create DO near user's location (lower latency)
- ✅ Data residency requirements (e.g., EU users → weur/eeur)
Limitations:
- ⚠️ Hints are best-effort - not guaranteed
- ⚠️ Only affects first creation - subsequent access uses existing location
- ⚠️ Cannot move existing DOs - once created, location is fixed
Jurisdiction Restriction (Data Residency)
Enforce strict data location requirements:
// Create DO that MUST stay in EU
const euId = env.MY_DO.newUniqueId({ jurisdiction: 'eu' });
// Available jurisdictions:
// - 'eu' - European Union
// - 'fedramp' - FedRAMP (US government)
Use for:
- Regulatory compliance (GDPR, FedRAMP)
- Data sovereignty requirements
CRITICAL:
- ✅ Strictly enforced - DO will never leave jurisdiction
- ⚠️ Cannot combine jurisdiction with location hints
- ⚠️ Higher latency for users outside jurisdiction
Migrations - Managing DO Classes
Migrations are REQUIRED when you:
- Create a new DO class
- Rename a DO class
- Delete a DO class
- Transfer a DO class to another Worker
Migration Types:
1. Create New DO Class
{
"durable_objects": {
"bindings": [
{
"name": "COUNTER",
"class_name": "Counter"
}
]
},
"migrations": [
{
"tag": "v1", // Unique identifier for this migration
"new_sqlite_classes": [ // SQLite backend (recommended)
"Counter"
]
}
]
}
For KV backend (legacy):
{
"migrations": [
{
"tag": "v1",
"new_classes": ["Counter"] // KV backend (128MB limit)
}
]
}
CRITICAL:
- ✅ Use
new_sqlite_classesfor new DOs (up to 1GB storage) - ❌ Cannot enable SQLite on existing deployed KV-backed DOs
2. Rename DO Class
{
"durable_objects": {
"bindings": [
{
"name": "MY_DO",
"class_name": "NewClassName" // New class name
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["OldClassName"]
},
{
"tag": "v2", // New migration tag
"renamed_classes": [
{
"from": "OldClassName",
"to": "NewClassName"
}
]
}
]
}
What happens:
- ✅ Existing DO instances keep their data
- ✅ Old bindings automatically forward to new class
- ⚠️ Must export new class in Worker code
3. Delete DO Class
{
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["Counter"]
},
{
"tag": "v2",
"deleted_classes": ["Counter"] // Mark as deleted
}
]
}
What happens:
- ✅ Existing DO instances are deleted immediately
- ✅ All storage is deleted
- ⚠️ Cannot undo - data is permanently lost
Before deleting:
- Export data if needed
- Update Workers that reference this DO
4. Transfer DO Class to Another Worker
// In destination Worker:
{
"durable_objects": {
"bindings": [
{
"name": "TRANSFERRED_DO",
"class_name": "TransferredClass"
}
]
},
"migrations": [
{
"tag": "v1",
"transferred_classes": [
{
"from": "OriginalClass",
"from_script": "original-worker", // Source Worker name
"to": "TransferredClass"
}
]
}
]
}
What happens:
- ✅ DO instances move to new Worker
- ✅ All storage is transferred
- ✅ Old bindings automatically forward
- ⚠️ Destination class must be exported
Migration Rules
CRITICAL Migration Gotchas:
❌ Migrations are ATOMIC - cannot gradual deploy
- All instances migrate at once when you deploy
- No partial rollout support
❌ Migration tags must be unique
- Cannot reuse tags
- Tags are append-only
❌ Cannot enable SQLite on existing KV-backed DOs
- Must create new DO class instead
✅ Code changes don't need migrations
- Only schema changes (new/rename/delete/transfer) need migrations
- You can deploy code updates freely
✅ Global uniqueness is per account
- DO class names are unique across your entire account
- Even across different Workers
Common Patterns
Pattern 1: Rate Limiting (Per-User)
export class RateLimiter extends DurableObject {
async checkLimit(userId: string, limit: number, window: number): Promise<boolean> {
const key = `rate:${userId}`;
const now = Date.now();
// Get recent requests
const requests = await this.ctx.storage.get<number[]>(key) || [];
// Remove requests outside window
const validRequests = requests.filter(timestamp => now - timestamp < window);
// Check limit
if (validRequests.length >= limit) {
return false; // Rate limit exceeded
}
// Add current request
validRequests.push(now);
await this.ctx.storage.put(key, validRequests);
return true; // Within limit
}
}
// Worker usage:
const limiter = env.RATE_LIMITER.getByName(userId);
const allowed = await limiter.checkLimit(userId, 100, 60000); // 100 req/min
if (!allowed) {
return new Response('Rate limit exceeded', { status: 429 });
}
Pattern 2: Session Management
export class UserSession 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 session (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
expires_at INTEGER
);
`);
// Schedule cleanup alarm
ctx.blockConcurrencyWhile(async () => {
const alarm = await ctx.storage.getAlarm();
if (alarm === null) {
await ctx.storage.setAlarm(Date.now() + 3600000); // 1 hour
}
});
}
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 get(key: string): Promise<any | null> {
const cursor = this.sql.exec(
'SELECT value, expires_at FROM session WHERE key = ?',
key
);
const row = cursor.one<{ value: string; expires_at: number | null }>({ allowNone: true });
if (!row) {
return null;
}
// Check expiration
if (row.expires_at && row.expires_at < Date.now()) {
this.sql.exec('DELETE FROM session WHERE key = ?', key);
return null;
}
return JSON.parse(row.value);
}
async alarm(): Promise<void> {
// Cleanup expired sessions
this.sql.exec('DELETE FROM session WHERE expires_at < ?', Date.now());
// Schedule next cleanup
await this.ctx.storage.setAlarm(Date.now() + 3600000);
}
}
Pattern 3: Leader Election
export class LeaderElection 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 leader (
id INTEGER PRIMARY KEY CHECK (id = 1),
worker_id TEXT NOT NULL,
elected_at INTEGER NOT NULL
);
`);
}
async electLeader(workerId: string): Promise<boolean> {
// Try to become leader
try {
this.sql.exec(
'INSERT INTO leader (id, worker_id, elected_at) VALUES (1, ?, ?)',
workerId,
Date.now()
);
return true; // Became leader
} catch (error) {
return false; // Someone else is leader
}
}
async getLeader(): Promise<string | null> {
const cursor = this.sql.exec('SELECT worker_id FROM leader WHERE id = 1');
const row = cursor.one<{ worker_id: string }>({ allowNone: true });
return row?.worker_id || null;
}
async releaseLeadership(workerId: string): Promise<void> {
this.sql.exec('DELETE FROM leader WHERE id = 1 AND worker_id = ?', workerId);
}
}
Pattern 4: Multi-DO Coordination
// Coordinator DO
export class GameCoordinator extends DurableObject {
async createGame(gameId: string, env: Env): Promise<void> {
// Create game room DO
const gameRoom = env.GAME_ROOM.getByName(gameId);
await gameRoom.initialize();
// Track in coordinator
await this.ctx.storage.put(`game:${gameId}`, {
id: gameId,
created: Date.now(),
});
}
async listGames(): Promise<string[]> {
const games = await this.ctx.storage.list({ prefix: 'game:' });
return Array.from(games.keys()).map(key => key.replace('game:', ''));
}
}
// Game room DO
export class GameRoom extends DurableObject {
async initialize(): Promise<void> {
await this.ctx.storage.put('state', {
players: [],
started: false,
});
}
async addPlayer(playerId: string): Promise<void> {
const state = await this.ctx.storage.get('state');
state.players.push(playerId);
await this.ctx.storage.put('state', state);
}
}
Critical Rules
Always Do
✅ Export DO class from Worker
export class MyDO extends DurableObject { }
export default MyDO; // Required
✅ Call super(ctx, env) in constructor
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env); // Required first line
}
✅ Use new_sqlite_classes for new DOs
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }
✅ Use ctx.acceptWebSocket() for hibernation
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 setTimeout or setInterval
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 15+ 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: https://developers.cloudflare.com/durable-objects/best-practices/websockets/ Why It Happens: Outgoing WebSockets don't support hibernation Prevention: Only use hibernation for server-side (incoming) WebSockets
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: Partial deleteAll on KV Backend
Error: Storage not fully deleted, billing continues
Source: https://developers.cloudflare.com/durable-objects/api/legacy-kv-storage-api/
Why It Happens: KV backend deleteAll() can fail partially
Prevention: Use SQLite backend for atomic deleteAll
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
Configuration Reference
Complete wrangler.jsonc Example
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-22",
// Durable Objects configuration
"durable_objects": {
"bindings": [
{
"name": "COUNTER", // Binding name (use as env.COUNTER)
"class_name": "Counter" // Must match exported class
},
{
"name": "CHAT_ROOM",
"class_name": "ChatRoom"
}
]
},
// Migrations (required for all DO changes)
"migrations": [
{
"tag": "v1", // Initial migration
"new_sqlite_classes": [
"Counter",
"ChatRoom"
]
},
{
"tag": "v2", // Rename example
"renamed_classes": [
{
"from": "Counter",
"to": "CounterV2"
}
]
}
]
}
TypeScript Types
import { DurableObject, DurableObjectState, DurableObjectNamespace } from 'cloudflare:workers';
// Environment bindings
interface Env {
MY_DO: DurableObjectNamespace<MyDurableObject>;
DB: D1Database;
// ... other bindings
}
// Durable Object class
export class MyDurableObject extends DurableObject<Env> {
sql: SqlStorage;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
}
async myMethod(): Promise<string> {
// Access env bindings
await this.env.DB.prepare('...').run();
return 'Hello';
}
}
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
references/top-errors.mdfor common problems - Review
templates/for working examples - Consult official docs: https://developers.cloudflare.com/durable-objects/
- Verify migrations configuration carefully
GitHub Repository
Related Skills
sglang
MetaSGLang is a high-performance LLM serving framework that specializes in fast, structured generation for JSON, regex, and agentic workflows using its RadixAttention prefix caching. It delivers significantly faster inference, especially for tasks with repeated prefixes, making it ideal for complex, structured outputs and multi-turn conversations. Choose SGLang over alternatives like vLLM when you need constrained decoding or are building applications with extensive prefix sharing.
evaluating-llms-harness
TestingThis Claude Skill runs the lm-evaluation-harness to benchmark LLMs across 60+ standardized academic tasks like MMLU and GSM8K. It's designed for developers to compare model quality, track training progress, or report academic results. The tool supports various backends including HuggingFace and vLLM models.
llamaguard
OtherLlamaGuard is Meta's 7-8B parameter model for moderating LLM inputs and outputs across six safety categories like violence and hate speech. It offers 94-95% accuracy and can be deployed using vLLM, Hugging Face, or Amazon SageMaker. Use this skill to easily integrate content filtering and safety guardrails into your AI applications.
langchain
MetaLangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.
