🎯

cloudflare-durable-objects

🎯Skill

from jackspace/claudeskillz

VibeIndex|
What it does

Enables globally unique, stateful coordination and real-time communication using Cloudflare Durable Objects for WebSocket, chat rooms, and persistent state management.

πŸ“¦

Part of

jackspace/claudeskillz(219 items)

cloudflare-durable-objects

Installation

npm runRun npm script
npm run dev
npxRun with npx
npx wrangler deploy
πŸ“– Extracted from docs: jackspace/claudeskillz
6Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

|

Overview

# Cloudflare Durable Objects

Status: Production Ready βœ…

Last Updated: 2025-10-22

Dependencies: cloudflare-worker-base (recommended)

Latest Versions: wrangler@4.43.0+, @cloudflare/workers-types@4.20251014.0+

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

```bash

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

```bash

cd my-existing-worker

npm install -D @cloudflare/workers-types

```

Create a Durable Object class (src/counter.ts):

```typescript

import { DurableObject } from 'cloudflare:workers';

export class Counter extends DurableObject {

async increment(): Promise {

// 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 {

return (await this.ctx.storage.get('value')) || 0;

}

}

// CRITICAL: Export the class

export default Counter;

```

Configure wrangler.jsonc:

```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):

```typescript

import { Counter } from './counter';

interface Env {

COUNTER: DurableObjectNamespace;

}

export { Counter };

export default {

async fetch(request: Request, env: Env): Promise {

// 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:

```bash

npx wrangler deploy

```

---

Durable Object Class Structure

Base Class Pattern

All Durable Objects MUST extend DurableObject from cloudflare:workers:

```typescript

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 {

return 'Hello from DO!';

}

// Optional: HTTP fetch handler

async fetch(request: Request): Promise {

return new Response('Hello from DO fetch!');

}

}

// CRITICAL: Export the class

export default MyDurableObject;

```

Constructor Pattern

```typescript

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

```typescript

// 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:

```typescript

// 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;

}

```

---

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:

```jsonc

{

"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:

```typescript

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 {

// 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 {

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 {

this.sql.exec(

'DELETE FROM messages WHERE created_at < ?',

beforeTimestamp

);

}

}

```

SQL API Methods:

```typescript

// Execute query (returns cursor)

const cursor = this.sql.exec('SELECT * FROM table WHERE id = ?', id);

// Get single row

const row = cursor.one();

// Get first row or null

const row = cursor.one({ allowNone: true });

// Get all rows as array

const rows = cursor.toArray();

// 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:

```typescript

import { DurableObject } from 'cloudflare:workers';

export class MyDurableObject extends DurableObject {

async increment(): Promise {

// Get value

let count = await this.ctx.storage.get('count') || 0;

// Increment

count += 1;

// Put value back

await this.ctx.storage.put('count', count);

return count;

}

async batchOperations(): Promise {

// Get multiple keys

const map = await this.ctx.storage.get(['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 {

// 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 {

// 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:

```typescript

// Get single value

const value = await this.ctx.storage.get('key');

// Get multiple values (returns Map)

const map = await this.ctx.storage.get(['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({

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

```typescript

import { DurableObject } from 'cloudflare:workers';

export class ChatRoom extends DurableObject {

sessions: Map;

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 {

// 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 {

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 {

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 {

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:

```typescript

// Receive message from client

async webSocketMessage(

ws: WebSocket,

message: string | ArrayBuffer

): Promise {

// Handle message

}

// WebSocket closed by client

async webSocketClose(

ws: WebSocket,

code: number,

reason: string,

wasClean: boolean

): Promise {

// Cleanup

}

// WebSocket error occurred

async webSocketError(

ws: WebSocket,

error: any

): Promise {

// Handle error

}

```

Hibernation-Safe Patterns:

```typescript

// βœ… 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

```typescript

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 {

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 {

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 {

// Send to external API, write to database, etc.

console.log(Processing ${items.length} items:, items);

}

}

```

Alarm API Methods:

```typescript

// 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 {

// 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):

```typescript

async alarm(alarmInfo: { retryCount: number; isRetry: boolean }): Promise {

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:

```jsonc

{

"compatibility_date": "2025-10-22"

}

```

Define RPC methods on DO class:

```typescript

export class Counter extends DurableObject {

// Public RPC methods (automatically exposed)

async increment(): Promise {

let value = await this.ctx.storage.get('count') || 0;

value += 1;

await this.ctx.storage.put('count', value);

return value;

}

async decrement(): Promise {

let value = await this.ctx.storage.get('count') || 0;

value -= 1;

await this.ctx.storage.put('count', value);

return value;

}

async get(): Promise {

return await this.ctx.storage.get('count') || 0;

}

}

```

Call from Worker:

```typescript

// 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:

```typescript

export class Counter extends DurableObject {

async fetch(request: Request): Promise {

const url = new URL(request.url);

if (url.pathname === '/increment' && request.method === 'POST') {

let value = await this.ctx.storage.get('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('count') || 0;

return new Response(JSON.stringify({ count: value }));

}

return new Response('Not found', { status: 404 });

}

}

```

Call from Worker:

```typescript

// 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:

  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:

```typescript

// 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:

```typescript

// 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:

```typescript

// 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

```typescript

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

```typescript

// 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:

```typescript

// 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:

```typescript

// 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

```jsonc

{

"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):

```jsonc

{

"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

```jsonc

{

"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

```jsonc

{

"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

```jsonc

// 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)

```typescript

export class RateLimiter extends DurableObject {

async checkLimit(userId: string, limit: number, window: number): Promise {

const key = rate:${userId};

const now = Date.now();

// Get recent requests

const requests = await this.ctx.storage.get(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

```typescript

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 {

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 {

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 {

// 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

```typescript

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 {

// 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 {

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 {

this.sql.exec('DELETE FROM leader WHERE id = 1 AND worker_id = ?', workerId);

}

}

```

Pattern 4: Multi-DO Coordination

```typescript

// Coordinator DO

export class GameCoordinator extends DurableObject {

async createGame(gameId: string, env: Env): Promise {

// 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 {

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 {

await this.ctx.storage.put('state', {

players: [],

started: false,

});

}

async addPlayer(playerId: string): Promise {

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

```typescript

export class MyDO extends DurableObject { }

export default MyDO; // Required

```

βœ… Call super(ctx, env) in constructor

```typescript

constructor(ctx: DurableObjectState, env: Env) {

super(ctx, env); // Required first line

}

```

βœ… Use new_sqlite_classes for new DOs

```jsonc

{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }

```

βœ… Use ctx.acceptWebSocket() for hibernation

```typescript

this.ctx.acceptWebSocket(server); // Enables hibernation

```

βœ… Persist critical state to storage (not just memory)

```typescript

await this.ctx.storage.put('important', value);

```

βœ… Use alarms instead of setTimeout/setInterval

```typescript

await this.ctx.storage.setAlarm(Date.now() + 60000);

```

βœ… Use parameterized SQL queries

```typescript

this.sql.exec('SELECT * FROM table WHERE id = ?', id);

```

βœ… Minimize constructor work

```typescript

constructor(ctx, env) {

super(ctx, env);

// Minimal initialization only

ctx.blockConcurrencyWhile(async () => {

// Load from storage

});

}

```

Never Do

❌ Create DO without migration

```jsonc

// Missing migrations array = error

```

❌ Forget to export DO class

```typescript

class MyDO extends DurableObject { }

// Missing: export default MyDO;

```

❌ Use setTimeout or setInterval

```typescript

setTimeout(() => {}, 1000); // Prevents hibernation

```

❌ Rely only on in-memory state with WebSockets

```typescript

// ❌ WRONG: this.sessions will be lost on hibernation

// βœ… CORRECT: Use serializeAttachment()

```

❌ Deploy migrations gradually

```bash

# Migrations are atomic - cannot use gradual rollout

```

❌ Enable SQLite on existing KV-backed DO

```jsonc

// Not supported - must create new DO class instead

```

❌ Use standard WebSocket API expecting hibernation

```typescript

ws.accept(); // ❌ No hibernation

this.ctx.acceptWebSocket(ws); // βœ… Hibernation enabled

```

❌ Assume location hints are guaranteed

```typescript

// 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:

```typescript

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

```jsonc

{

"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()

```typescript

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

```typescript

// ❌ 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

```typescript

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

```jsonc

{ "bindings": [{ "name": "MY_DO", "class_name": "MyDO" }] }

```

```typescript

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

```typescript

async alarm(info: { retryCount: number }): Promise {

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

```jsonc

{

"$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

```typescript

import { DurableObject, DurableObjectState, DurableObjectNamespace } from 'cloudflare:workers';

// Environment bindings

interface Env {

MY_DO: DurableObjectNamespace;

DB: D1Database;

// ... other bindings

}

// Durable Object class

export class MyDurableObject extends DurableObject {

sql: SqlStorage;

constructor(ctx: DurableObjectState, env: Env) {

super(ctx, env);

this.sql = ctx.storage.sql;

}

async myMethod(): Promise {

// 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?

  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