🎯

cloudflare-durable-objects

🎯Skill

from ovachiever/droid-tings

VibeIndex|
What it does

Enables building stateful, real-time Cloudflare Durable Objects for persistent coordination, WebSocket servers, and complex application state management.

πŸ“¦

Part of

ovachiever/droid-tings(370 items)

cloudflare-durable-objects

Installation

git cloneClone repository
git clone https://github.com/ovachiever/droid-tings.git
πŸ“– Extracted from docs: ovachiever/droid-tings
14Installs
20
-
AddedFeb 4, 2026

Skill Details

SKILL.md

|

Overview

# Cloudflare Durable Objects

Status: Production Ready βœ…

Last Updated: 2025-11-23

Dependencies: cloudflare-worker-base (recommended)

Latest Versions: wrangler@4.50.0, @cloudflare/workers-types@4.20251121.0

Official Docs: https://developers.cloudflare.com/durable-objects/

Recent Updates (2025):

  • Oct 2025: WebSocket message size 1 MiB β†’ 32 MiB, Data Studio UI for SQLite DOs (view/edit storage in dashboard)
  • Aug 2025: getByName() API shortcut for named DOs
  • June 2025: @cloudflare/actors library (beta) - recommended SDK with migrations, alarms, Actor class pattern
  • May 2025: Python Workers support for Durable Objects
  • April 2025: SQLite GA with 10GB storage (beta β†’ GA, 1GB β†’ 10GB), Free tier access
  • Feb 2025: PRAGMA optimize support, improved error diagnostics with reference IDs

---

Quick Start

Scaffold new DO project:

```bash

npm create cloudflare@latest my-durable-app -- --template=cloudflare/durable-objects-template --ts

```

Or add to existing Worker:

```typescript

// src/counter.ts - Durable Object class

import { DurableObject } from 'cloudflare:workers';

export class Counter extends DurableObject {

async increment(): Promise {

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

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

return value;

}

}

export default Counter; // CRITICAL: Export required

```

```jsonc

// wrangler.jsonc - Configuration

{

"durable_objects": {

"bindings": [{ "name": "COUNTER", "class_name": "Counter" }]

},

"migrations": [

{ "tag": "v1", "new_sqlite_classes": ["Counter"] } // SQLite backend (10GB limit)

]

}

```

```typescript

// src/index.ts - Worker

import { Counter } from './counter';

export { Counter };

export default {

async fetch(request: Request, env: { COUNTER: DurableObjectNamespace }) {

const stub = env.COUNTER.getByName('global-counter'); // Aug 2025: getByName() shortcut

return new Response(Count: ${await stub.increment()});

}

};

```

---

DO Class Essentials

```typescript

import { DurableObject } from 'cloudflare:workers';

export class MyDO extends DurableObject {

constructor(ctx: DurableObjectState, env: Env) {

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

// Load state before requests (optional)

ctx.blockConcurrencyWhile(async () => {

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

});

}

// RPC methods (recommended)

async myMethod(): Promise { return 'Hello'; }

// HTTP fetch handler (optional)

async fetch(request: Request): Promise { return new Response('OK'); }

}

export default MyDO; // CRITICAL: Export required

// Worker must export DO class too

import { MyDO } from './my-do';

export { MyDO };

```

Constructor Rules:

  • βœ… Call super(ctx, env) first
  • βœ… Keep minimal - heavy work blocks hibernation wake
  • βœ… Use ctx.blockConcurrencyWhile() for storage initialization
  • ❌ Never setTimeout/setInterval (use alarms)
  • ❌ Don't rely on in-memory state with WebSockets (persist to storage)

---

Storage API

Two backends available:

  • SQLite (recommended): 10GB storage, SQL queries, atomic operations, PITR
  • KV: 128MB storage, key-value only

Enable SQLite in migrations:

```jsonc

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

```

SQL API (SQLite backend)

```typescript

export class MyDO extends DurableObject {

sql: SqlStorage;

constructor(ctx: DurableObjectState, env: Env) {

super(ctx, env);

this.sql = ctx.storage.sql;

this.sql.exec(`

CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY, text TEXT, created_at INTEGER);

CREATE INDEX IF NOT EXISTS idx_created ON messages(created_at);

PRAGMA optimize; // Feb 2025: Query performance optimization

`);

}

async addMessage(text: string): Promise {

const cursor = this.sql.exec('INSERT INTO messages (text, created_at) VALUES (?, ?) RETURNING id', text, Date.now());

return cursor.one<{ id: number }>().id;

}

async getMessages(limit = 50): Promise {

return this.sql.exec('SELECT * FROM messages ORDER BY created_at DESC LIMIT ?', limit).toArray();

}

}

```

SQL Methods:

  • sql.exec(query, ...params) β†’ cursor
  • cursor.one() β†’ single row (throws if none)
  • cursor.one({ allowNone: true }) β†’ row or null
  • cursor.toArray() β†’ all rows
  • ctx.storage.transactionSync(() => { ... }) β†’ atomic multi-statement

Rules: Always use ? placeholders, create indexes, use PRAGMA optimize after schema changes

Key-Value API (both backends)

```typescript

// Single operations

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

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

await this.ctx.storage.delete('key');

// Batch operations

await this.ctx.storage.put({ key1: val1, key2: val2 });

const map = await this.ctx.storage.get(['key1', 'key2']);

await this.ctx.storage.delete(['key1', 'key2']);

// List and delete all

const map = await this.ctx.storage.list({ prefix: 'user:', limit: 100 });

await this.ctx.storage.deleteAll(); // Atomic on SQLite only

// Transactions

await this.ctx.storage.transaction(async (txn) => {

await txn.put('key1', val1);

await txn.put('key2', val2);

});

```

Storage Limits: SQLite 10GB (April 2025 GA) | KV 128MB

---

WebSocket Hibernation API

Capabilities:

  • Thousands of WebSocket connections per instance
  • Hibernate when idle (~10s no activity) to save costs
  • Auto wake-up when messages arrive
  • Message size limit: 32 MiB (Oct 2025, up from 1 MiB)

How it works:

  1. Active β†’ handles messages
  2. Idle β†’ ~10s no activity
  3. Hibernation β†’ in-memory state cleared, WebSockets stay connected
  4. Wake β†’ message arrives β†’ constructor runs β†’ handler called

CRITICAL: In-memory state is lost on hibernation. Use serializeAttachment() to persist per-WebSocket metadata.

Hibernation-Safe Pattern

```typescript

export class ChatRoom extends DurableObject {

sessions: Map;

constructor(ctx: DurableObjectState, env: Env) {

super(ctx, env);

this.sessions = new Map();

// CRITICAL: Restore WebSocket metadata after hibernation

ctx.getWebSockets().forEach((ws) => {

this.sessions.set(ws, ws.deserializeAttachment());

});

}

async fetch(request: Request): Promise {

const pair = new WebSocketPair();

const [client, server] = Object.values(pair);

const url = new URL(request.url);

const metadata = { userId: url.searchParams.get('userId'), username: url.searchParams.get('username') };

// CRITICAL: Use ctx.acceptWebSocket(), NOT ws.accept()

this.ctx.acceptWebSocket(server);

server.serializeAttachment(metadata); // Persist across hibernation

this.sessions.set(server, metadata);

return new Response(null, { status: 101, webSocket: client });

}

async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise {

const session = this.sessions.get(ws);

// Handle message (max 32 MiB since Oct 2025)

}

async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean): Promise {

this.sessions.delete(ws);

ws.close(code, 'Closing');

}

async webSocketError(ws: WebSocket, error: any): Promise {

this.sessions.delete(ws);

}

}

```

Hibernation Rules:

  • βœ… ctx.acceptWebSocket(ws) - enables hibernation
  • βœ… ws.serializeAttachment(data) - persist metadata
  • βœ… ctx.getWebSockets().forEach() - restore in constructor
  • βœ… Use alarms instead of setTimeout/setInterval
  • ❌ ws.accept() - standard API, no hibernation
  • ❌ setTimeout/setInterval - prevents hibernation
  • ❌ In-progress fetch() - blocks hibernation

---

Alarms API

Schedule DO to wake at future time. Use for: batching, cleanup, reminders, periodic tasks.

```typescript

export class Batcher extends DurableObject {

async addItem(item: string): Promise {

// Add to buffer

const buffer = await this.ctx.storage.get('buffer') || [];

buffer.push(item);

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

// Schedule alarm if not set

if ((await this.ctx.storage.getAlarm()) === null) {

await this.ctx.storage.setAlarm(Date.now() + 10000); // 10 seconds

}

}

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

if (info.retryCount > 3) return; // Give up after 3 retries

const buffer = await this.ctx.storage.get('buffer') || [];

await this.processBatch(buffer);

await this.ctx.storage.put('buffer', []);

// Alarm auto-deleted after success

}

}

```

API Methods:

  • await ctx.storage.setAlarm(Date.now() + 60000) - set alarm (overwrites existing)
  • await ctx.storage.getAlarm() - get timestamp or null
  • await ctx.storage.deleteAlarm() - cancel alarm
  • async alarm(info) - handler called when alarm fires

Behavior:

  • βœ… At-least-once execution, auto-retries (up to 6x, exponential backoff)
  • βœ… Survives hibernation/eviction
  • βœ… Auto-deleted after success
  • ⚠️ One alarm per DO (new alarm overwrites)

---

RPC vs HTTP Fetch

RPC (Recommended): Direct method calls, type-safe, simple

```typescript

// DO class

export class Counter extends DurableObject {

async increment(): Promise {

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

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

return value;

}

}

// Worker calls

const stub = env.COUNTER.getByName('my-counter');

const count = await stub.increment(); // Type-safe!

```

HTTP Fetch: Request/response pattern, required for WebSocket upgrades

```typescript

// DO class

export class Counter extends DurableObject {

async fetch(request: Request): Promise {

const url = new URL(request.url);

if (url.pathname === '/increment') {

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

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

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

}

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

}

}

// Worker calls

const stub = env.COUNTER.getByName('my-counter');

const response = await stub.fetch('https://fake-host/increment', { method: 'POST' });

const data = await response.json();

```

When to use: RPC for new projects (simpler), HTTP Fetch for WebSocket upgrades or complex routing

---

Getting DO Stubs

Three ways to get IDs:

  1. idFromName(name) - Consistent routing (same name = same DO)

```typescript

const stub = env.CHAT_ROOM.getByName('room-123'); // Aug 2025: Shortcut for idFromName + get

// Use for: chat rooms, user sessions, per-tenant logic, singletons

```

  1. newUniqueId() - Random unique ID (must store for reuse)

```typescript

const id = env.MY_DO.newUniqueId({ jurisdiction: 'eu' }); // Optional: EU compliance

const idString = id.toString(); // Save to KV/D1 for later

```

  1. idFromString(idString) - Recreate from saved ID

```typescript

const id = env.MY_DO.idFromString(await env.KV.get('session:123'));

const stub = env.MY_DO.get(id);

```

Location hints (best-effort):

```typescript

const stub = env.MY_DO.get(id, { locationHint: 'enam' }); // wnam, enam, sam, weur, eeur, apac, oc, afr, me

```

Jurisdiction (strict enforcement):

```typescript

const id = env.MY_DO.newUniqueId({ jurisdiction: 'eu' }); // Options: 'eu', 'fedramp'

// Cannot combine with location hints, higher latency outside jurisdiction

```

---

Migrations

Required for: create, rename, delete, transfer DO classes

1. Create:

```jsonc

{ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Counter"] }] } // SQLite 10GB

// Or: "new_classes": ["Counter"] // KV 128MB (legacy)

```

2. Rename:

```jsonc

{ "migrations": [

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

{ "tag": "v2", "renamed_classes": [{ "from": "OldName", "to": "NewName" }] }

]}

```

3. Delete:

```jsonc

{ "migrations": [

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

{ "tag": "v2", "deleted_classes": ["Counter"] } // Immediate deletion, cannot undo

]}

```

4. Transfer:

```jsonc

{ "migrations": [{ "tag": "v1", "transferred_classes": [

{ "from": "OldClass", "from_script": "old-worker", "to": "NewClass" }

]}]}

```

Migration Rules:

  • ❌ Atomic (all instances migrate at once, no gradual rollout)
  • ❌ Tags are unique and append-only
  • ❌ Cannot enable SQLite on existing KV-backed DOs
  • βœ… Code changes don't need migrations (only schema changes)
  • βœ… Class names globally unique per account

---

Common Patterns

Rate Limiting:

```typescript

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

const requests = (await this.ctx.storage.get(rate:${userId})) || [];

const valid = requests.filter(t => Date.now() - t < window);

if (valid.length >= limit) return false;

valid.push(Date.now());

await this.ctx.storage.put(rate:${userId}, valid);

return true;

}

```

Session Management with TTL:

```typescript

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 alarm(): Promise {

this.sql.exec('DELETE FROM session WHERE expires_at < ?', Date.now());

await this.ctx.storage.setAlarm(Date.now() + 3600000); // Hourly cleanup

}

```

Leader Election:

```typescript

async electLeader(workerId: string): Promise {

try {

this.sql.exec('INSERT INTO leader (id, worker_id, elected_at) VALUES (1, ?, ?)', workerId, Date.now());

return true;

} catch { return false; } // Already has leader

}

```

Multi-DO Coordination:

```typescript

// Coordinator delegates to child DOs

const gameRoom = env.GAME_ROOM.getByName(gameId);

await gameRoom.initialize();

await this.ctx.storage.put(game:${gameId}, { created: Date.now() });

```

---

Critical Rules

Always Do

βœ… Export DO class from Worker

```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 & Types

wrangler.jsonc:

```jsonc

{

"compatibility_date": "2025-11-23",

"durable_objects": {

"bindings": [{ "name": "COUNTER", "class_name": "Counter" }]

},

"migrations": [

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

{ "tag": "v2", "renamed_classes": [{ "from": "Counter", "to": "CounterV2" }] }

]

}

```

TypeScript:

```typescript

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

interface Env { MY_DO: DurableObjectNamespace; }

export class MyDurableObject extends DurableObject {

constructor(ctx: DurableObjectState, env: Env) {

super(ctx, env);

this.sql = ctx.storage.sql;

}

}

```

---

Official Documentation

  • Durable Objects: https://developers.cloudflare.com/durable-objects/
  • State API (SQL): https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
  • WebSocket Hibernation: https://developers.cloudflare.com/durable-objects/best-practices/websockets/
  • Alarms API: https://developers.cloudflare.com/durable-objects/api/alarms/
  • Migrations: https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/
  • Best Practices: https://developers.cloudflare.com/durable-objects/best-practices/
  • Pricing: https://developers.cloudflare.com/durable-objects/platform/pricing/

---

Questions? Issues?

  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