Claude
Skills
Sign in
Back

cloudflare-durable-objects

Included with Lifetime
$97 forever

Comprehensive guide for Cloudflare Durable Objects - globally unique, stateful objects for coordination, real-time communication, and persistent state management. Use when: building real-time applications, creating WebSocket servers with hibernation, implementing chat rooms or multiplayer games, coordinating between multiple clients, managing per-user or per-room state, implementing rate limiting or session management, scheduling tasks with alarms, building queues or workflows, or encountering "do class export", "new_sqlite_classes", "migrations required", "websocket hibernation", "alarm api error", or "global uniqueness" errors. Prevents 15+ documented issues: class not exported, missing migrations, wrong migration type, constructor overhead blocking hibernation, setTimeout breaking hibernation, in-memory state lost on hibernation, outgoing WebSocket not hibernating, global uniqueness confusion, partial deleteAll on KV backend, binding name mismatches, state size limits exceeded, non-atomic migrations, location hints misunderstood, alarm retry failures, and fetch calls blocking hibernation. Keywords: durable objects, cloudflare do, DurableObject class, do bindings, websocket hibernation, do state api, ctx.storage.sql, ctx.acceptWebSocket, webSocketMessage, alarm() handler, storage.setAlarm, idFromName, newUniqueId, getByName, DurableObjectStub, serializeAttachment, real-time cloudflare, multiplayer cloudflare, chat room workers, coordination cloudflare, stateful workers, new_sqlite_classes, do migrations, location hints, RPC methods, blockConcurrencyWhile, "do class export", "new_sqlite_classes", "migrations required", "websocket hibernation", "alarm api error", "global uniqueness", "binding not found"

Backend & APIs

What this skill does


# 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

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

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

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

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

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

```typescript
// Execute query (returns cursor)
const cursor = this.sql.exec('SELECT * FROM table WHERE id = ?', id

Related in Backend & APIs