Loading...
Loading...
Comprehensive guide for Cloudflare Durable Objects - globally unique, stateful objects for coordination, real-time communication, and persistent state management. Use when: building real-time applications, creating WebSocket servers with hibernation, implementing chat rooms or multiplayer games, coordinating between multiple clients, managing per-user or per-room state, implementing rate limiting or session management, scheduling tasks with alarms, building queues or workflows, or encountering "do class export", "new_sqlite_classes", "migrations required", "websocket hibernation", "alarm api error", or "global uniqueness" errors. Prevents 15+ documented issues: class not exported, missing migrations, wrong migration type, constructor overhead blocking hibernation, setTimeout breaking hibernation, in-memory state lost on hibernation, outgoing WebSocket not hibernating, global uniqueness confusion, partial deleteAll on KV backend, binding name mismatches, state size limits exceeded, non-atomic migrations, location hints misunderstood, alarm retry failures, and fetch calls blocking hibernation. Keywords: durable objects, cloudflare do, DurableObject class, do bindings, websocket hibernation, do state api, ctx.storage.sql, ctx.acceptWebSocket, webSocketMessage, alarm() handler, storage.setAlarm, idFromName, newUniqueId, getByName, DurableObjectStub, serializeAttachment, real-time cloudflare, multiplayer cloudflare, chat room workers, coordination cloudflare, stateful workers, new_sqlite_classes, do migrations, location hints, RPC methods, blockConcurrencyWhile, "do class export", "new_sqlite_classes", "migrations required", "websocket hibernation", "alarm api error", "global uniqueness", "binding not found"
npx skill4agent add jackspace/claudeskillz cloudflare-durable-objectsnpm create cloudflare@latest my-durable-app -- \
--template=cloudflare/durable-objects-template \
--ts \
--git \
--deploy false
cd my-durable-app
npm install
npm run devcd my-existing-worker
npm install -D @cloudflare/workers-typessrc/counter.tsimport { 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;{
"$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"
]
}
]
}src/index.tsimport { 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}`);
},
};npx wrangler deployDurableObjectcloudflare:workersimport { 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(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)
}super(ctx, env)ctx.blockConcurrencyWhile()setTimeoutsetInterval// Export as default (required for Worker to use it)
export default MyDurableObject;
// Also export as named export (for type inference in Worker)
export { MyDurableObject };// 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>;
}wrangler.jsonc{
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MyDurableObject"] // ← Use this for SQLite
}
]
}ctx.storage.sqlimport { 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
);
}
}// 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
});?__cf_kvctx.storageimport { 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();
}
}// 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
});serializeAttachment()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 }));
}
});
}
}// 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
}// ✅ 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);setTimeoutsetIntervalfetch()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);
}
}// 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
}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
}
}>= 2024-04-03{
"compatibility_date": "2025-10-22"
}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;
}
}// 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();fetch()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 });
}
}// 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();| 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) |
idFromName(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');newUniqueId()// 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);idFromString(idString)// 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);DurableObjectNamespaceget(id)const id = env.MY_DO.idFromName('my-instance');
const stub = env.MY_DO.get(id);
// Call methods
await stub.myMethod();getByName(name)// 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();// 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// Create DO that MUST stay in EU
const euId = env.MY_DO.newUniqueId({ jurisdiction: 'eu' });
// Available jurisdictions:
// - 'eu' - European Union
// - 'fedramp' - FedRAMP (US government){
"durable_objects": {
"bindings": [
{
"name": "COUNTER",
"class_name": "Counter"
}
]
},
"migrations": [
{
"tag": "v1", // Unique identifier for this migration
"new_sqlite_classes": [ // SQLite backend (recommended)
"Counter"
]
}
]
}{
"migrations": [
{
"tag": "v1",
"new_classes": ["Counter"] // KV backend (128MB limit)
}
]
}new_sqlite_classes{
"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"
}
]
}
]
}{
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["Counter"]
},
{
"tag": "v2",
"deleted_classes": ["Counter"] // Mark as deleted
}
]
}// 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"
}
]
}
]
}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 });
}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);
}
}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);
}
}// 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);
}
}export class MyDO extends DurableObject { }
export default MyDO; // Requiredsuper(ctx, env)constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env); // Required first line
}new_sqlite_classes{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }ctx.acceptWebSocket()this.ctx.acceptWebSocket(server); // Enables hibernationawait this.ctx.storage.put('important', value);await this.ctx.storage.setAlarm(Date.now() + 60000);this.sql.exec('SELECT * FROM table WHERE id = ?', id);constructor(ctx, env) {
super(ctx, env);
// Minimal initialization only
ctx.blockConcurrencyWhile(async () => {
// Load from storage
});
}// Missing migrations array = errorclass MyDO extends DurableObject { }
// Missing: export default MyDO;setTimeoutsetIntervalsetTimeout(() => {}, 1000); // Prevents hibernation// ❌ WRONG: this.sessions will be lost on hibernation
// ✅ CORRECT: Use serializeAttachment()# Migrations are atomic - cannot use gradual rollout// Not supported - must create new DO class insteadws.accept(); // ❌ No hibernation
this.ctx.acceptWebSocket(ws); // ✅ Hibernation enabled// Location hints are best-effort only"binding not found""Class X not found"export class MyDO extends DurableObject { }
export default MyDO; // ← Required"migrations required""no migration found for class"{
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }
]
}new_classesnew_sqlite_classesnew_sqlite_classesblockConcurrencyWhile()constructor(ctx, env) {
super(ctx, env);
ctx.blockConcurrencyWhile(async () => {
// Load from storage
});
}setTimeoutsetInterval// ❌ WRONG
setTimeout(() => {}, 1000);
// ✅ CORRECT
await this.ctx.storage.setAlarm(Date.now() + 1000);serializeAttachment()ws.serializeAttachment({ userId, username });
// Restore in constructor
ctx.getWebSockets().forEach(ws => {
const metadata = ws.deserializeAttachment();
this.sessions.set(ws, metadata);
});deleteAll(){ "bindings": [{ "name": "MY_DO", "class_name": "MyDO" }] }env.MY_DO.getByName('instance'); // Must match binding name"state limit exceeded"async alarm(info: { retryCount: number }): Promise<void> {
if (info.retryCount > 3) {
console.error('Giving up after 3 retries');
return;
}
// Idempotent operation
}fetch(){
"$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"
}
]
}
]
}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';
}
}references/top-errors.mdtemplates/