home / skills / cloudflare / skills / durable-objects

durable-objects skill

/durable-objects

This skill helps you design, review, and test Cloudflare Durable Objects for stateful coordination, RPC, and durable storage at the edge.

npx playbooks add skill cloudflare/skills --skill durable-objects

Review the files below or copy the command above to add this skill to your agents.

Files (4)
SKILL.md
5.1 KB
---
name: durable-objects
description: Create and review Cloudflare Durable Objects. Use when building stateful coordination (chat rooms, multiplayer games, booking systems), implementing RPC methods, SQLite storage, alarms, WebSockets, or reviewing DO code for best practices. Covers Workers integration, wrangler config, and testing with Vitest.
---

# Durable Objects

Build stateful, coordinated applications on Cloudflare's edge using Durable Objects.

## When to Use

- Creating new Durable Object classes for stateful coordination
- Implementing RPC methods, alarms, or WebSocket handlers
- Reviewing existing DO code for best practices
- Configuring wrangler.jsonc/toml for DO bindings and migrations
- Writing tests with `@cloudflare/vitest-pool-workers`
- Designing sharding strategies and parent-child relationships

## Reference Documentation

- `./references/rules.md` - Core rules, storage, concurrency, RPC, alarms
- `./references/testing.md` - Vitest setup, unit/integration tests, alarm testing
- `./references/workers.md` - Workers handlers, types, wrangler config, observability

Search: `blockConcurrencyWhile`, `idFromName`, `getByName`, `setAlarm`, `sql.exec`

## Core Principles

### Use Durable Objects For

| Need | Example |
|------|---------|
| Coordination | Chat rooms, multiplayer games, collaborative docs |
| Strong consistency | Inventory, booking systems, turn-based games |
| Per-entity storage | Multi-tenant SaaS, per-user data |
| Persistent connections | WebSockets, real-time notifications |
| Scheduled work per entity | Subscription renewals, game timeouts |

### Do NOT Use For

- Stateless request handling (use plain Workers)
- Maximum global distribution needs
- High fan-out independent requests

## Quick Reference

### Wrangler Configuration

```jsonc
// wrangler.jsonc
{
  "durable_objects": {
    "bindings": [{ "name": "MY_DO", "class_name": "MyDurableObject" }]
  },
  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyDurableObject"] }]
}
```

### Basic Durable Object Pattern

```typescript
import { DurableObject } from "cloudflare:workers";

export interface Env {
  MY_DO: DurableObjectNamespace<MyDurableObject>;
}

export class MyDurableObject extends DurableObject<Env> {
  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    ctx.blockConcurrencyWhile(async () => {
      this.ctx.storage.sql.exec(`
        CREATE TABLE IF NOT EXISTS items (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          data TEXT NOT NULL
        )
      `);
    });
  }

  async addItem(data: string): Promise<number> {
    const result = this.ctx.storage.sql.exec<{ id: number }>(
      "INSERT INTO items (data) VALUES (?) RETURNING id",
      data
    );
    return result.one().id;
  }
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const stub = env.MY_DO.getByName("my-instance");
    const id = await stub.addItem("hello");
    return Response.json({ id });
  },
};
```

## Critical Rules

1. **Model around coordination atoms** - One DO per chat room/game/user, not one global DO
2. **Use `getByName()` for deterministic routing** - Same input = same DO instance
3. **Use SQLite storage** - Configure `new_sqlite_classes` in migrations
4. **Initialize in constructor** - Use `blockConcurrencyWhile()` for schema setup only
5. **Use RPC methods** - Not fetch() handler (compatibility date >= 2024-04-03)
6. **Persist first, cache second** - Always write to storage before updating in-memory state
7. **One alarm per DO** - `setAlarm()` replaces any existing alarm

## Anti-Patterns (NEVER)

- Single global DO handling all requests (bottleneck)
- Using `blockConcurrencyWhile()` on every request (kills throughput)
- Storing critical state only in memory (lost on eviction/crash)
- Using `await` between related storage writes (breaks atomicity)
- Holding `blockConcurrencyWhile()` across `fetch()` or external I/O

## Stub Creation

```typescript
// Deterministic - preferred for most cases
const stub = env.MY_DO.getByName("room-123");

// From existing ID string
const id = env.MY_DO.idFromString(storedIdString);
const stub = env.MY_DO.get(id);

// New unique ID - store mapping externally
const id = env.MY_DO.newUniqueId();
const stub = env.MY_DO.get(id);
```

## Storage Operations

```typescript
// SQL (synchronous, recommended)
this.ctx.storage.sql.exec("INSERT INTO t (c) VALUES (?)", value);
const rows = this.ctx.storage.sql.exec<Row>("SELECT * FROM t").toArray();

// KV (async)
await this.ctx.storage.put("key", value);
const val = await this.ctx.storage.get<Type>("key");
```

## Alarms

```typescript
// Schedule (replaces existing)
await this.ctx.storage.setAlarm(Date.now() + 60_000);

// Handler
async alarm(): Promise<void> {
  // Process scheduled work
  // Optionally reschedule: await this.ctx.storage.setAlarm(...)
}

// Cancel
await this.ctx.storage.deleteAlarm();
```

## Testing Quick Start

```typescript
import { env } from "cloudflare:test";
import { describe, it, expect } from "vitest";

describe("MyDO", () => {
  it("should work", async () => {
    const stub = env.MY_DO.getByName("test");
    const result = await stub.addItem("test");
    expect(result).toBe(1);
  });
});
```

Overview

This skill helps create, configure, and review Cloudflare Durable Objects for stateful, coordinated edge applications. It guides building DO classes, RPC methods, SQLite-backed storage, alarms, and WebSocket handlers, and includes Wrangler configuration and Vitest testing patterns. Use it to ensure correct concurrency, deterministic routing, and durable storage practices.

How this skill works

It inspects Durable Object code patterns, Wrangler bindings/migrations, storage usage (SQLite and KV), alarm scheduling, and fetch vs RPC usage. It enforces core rules like blockConcurrencyWhile() usage in constructors, getByName() deterministic routing, SQL usage for persistence, and one-alarm-per-DO semantics. It also provides testing guidance with @cloudflare/vitest-pool-workers and checks best-practice anti-patterns.

When to use it

  • Building new Durable Object classes for coordinated state (chat rooms, game instances, per-entity state)
  • Implementing RPC methods, WebSocket handlers, alarms, or SQLite-backed persistence
  • Reviewing existing DO implementations for concurrency, persistence, and routing correctness
  • Configuring wrangler.jsonc/toml bindings and migrations (new_sqlite_classes)
  • Writing unit and integration tests with Vitest and cloudflare:test emulation

Best practices

  • Model around coordination atoms: one DO per logical entity (room, user, game) not one global DO
  • Use getByName() for deterministic routing so same keys map to the same instance
  • Initialize schema inside constructor with blockConcurrencyWhile() only for setup work
  • Prefer ctx.storage.sql for synchronous SQLite operations and persist before mutating in-memory caches
  • Expose functionality via RPC methods rather than relying on fetch() handlers (compatibility date >= 2024-04-03)
  • Use a single alarm per DO; setAlarm replaces previous alarm and deleteAlarm cancels it

Example use cases

  • Chat room instance that manages participants, message ordering, and history with SQLite storage
  • Turn-based multiplayer game DO that enforces strong consistency and per-game timers via alarms
  • Booking or inventory entity that requires atomic updates and deterministic routing for the item
  • WebSocket-backed presence DO that maintains connections and pushes real-time events
  • Per-tenant SaaS entity storing configuration and usage counters with durable storage and RPC methods

FAQ

When should I use blockConcurrencyWhile()?

Only in the constructor for one-time initialization like schema creation; avoid using it for per-request work because it blocks concurrent execution and reduces throughput.

Should I store critical state only in memory?

No. Persist critical state to ctx.storage (SQL or KV) before updating in-memory caches to maintain durability on eviction or crash.