Back to Skills

cloudflare-durable-objects

jezweb
Updated Today
130 views
33
4
33
View on GitHub
Metawordaiapiautomationdesign

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 CommandRecommended
/plugin add https://github.com/jezweb/claude-skills
Git CloneAlternative
git clone https://github.com/jezweb/claude-skills.git ~/.claude/skills/cloudflare-durable-objects

Copy 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 setTimeout or setInterval - 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:

  1. SQL API (SQLite backend) - Recommended
  2. 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_kv table (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

  1. Active state - DO is in memory, handling messages
  2. Idle state - No messages for ~10 seconds, DO can hibernate
  3. Hibernation - In-memory state cleared, WebSockets stay connected to Cloudflare edge
  4. 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:

  • setTimeout or setInterval callbacks 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:

  1. RPC (Remote Procedure Call) - Recommended for new projects
  2. 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 CaseRecommendation
New project✅ RPC (simpler, type-safe)
HTTP request/response flowHTTP Fetch
Complex routing logicHTTP Fetch
Type safety important✅ RPC
Legacy compatibilityHTTP Fetch
WebSocket upgradesHTTP Fetch (required)

Creating Durable Object Stubs and Routing

To interact with a Durable Object from a Worker, you need to:

  1. Get a Durable Object ID
  2. Create a stub from the ID
  3. 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_classes for 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


Questions? Issues?

  1. Check references/top-errors.md for common problems
  2. Review templates/ for working examples
  3. Consult official docs: https://developers.cloudflare.com/durable-objects/
  4. Verify migrations configuration carefully

GitHub Repository

jezweb/claude-skills
Path: skills/cloudflare-durable-objects
aiautomationclaude-codeclaude-skillscloudflarereact

Related Skills

sglang

Meta

SGLang 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.

View skill

evaluating-llms-harness

Testing

This 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.

View skill

llamaguard

Other

LlamaGuard 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.

View skill

langchain

Meta

LangChain 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.

View skill