Preface

Building stateless APIs on Cloudflare Workers is straightforward: use a single fetch handler to process requests and offload data to KV, D1, or R2. This approach breaks down once your business logic requires things like “everyone in the same chat room must see the exact same messages”, “two users cannot book the same time slot”, or “scores and turn order in a match must be consistent across all players”. Regular Worker instances do not share memory, and external storage often uses eventual consistency; building your own locks, queues, and session stickiness comes with significant overhead.

Durable Objects (hereinafter referred to as DO) is Cloudflare’s primitive built for exactly these scenarios: each instance has a globally unique name, dedicated storage that travels with the instance, and execution contexts that can attach WebSockets, alarms, and RPC. The challenge lies in its numerous conventions and evolving interfaces. The getByName() method was only added to official documentation in August 2025; new namespaces now require SQLite backends; and after the compatibility date 2024-04-03, the official documentation recommends using RPC instead of continuing to write fetch() handlers on DOs. AI coding assistants relying solely on training data will often generate anti-patterns like “a single global DO handles all traffic”, “store critical state only in memory”, or “wrap every request with blockConcurrencyWhile()”.

Cloudflare maintains a dedicated Skill in the cloudflare/skills repository called durable-objects. Its purpose is not to repeat the product introduction, but to integrate sharding strategies, storage, concurrency, RPC, alarms, and Wrangler configuration into the agent’s workflow when creating, reviewing, and testing DOs, and to require that you first consult the current official documentation instead of relying solely on pre-trained knowledge.

What This Is

durable-objects is an official Agent Skill from Cloudflare, available at:

https://github.com/cloudflare/skills/tree/main/skills/durable-objects

The YAML header clearly defines its purpose: create and review Cloudflare Durable Objects; use it when building stateful coordination (chat rooms, multiplayer games, booking systems), implementing RPC, SQLite storage, alarms, WebSockets, or reviewing existing DO code according to best practices. It covers Workers integration, Wrangler configuration, and testing with Vitest. It also explicitly states: Prefer retrieving information from Cloudflare’s official documentation instead of relying on the model’s pre-trained knowledge.

It solves two specific problems:
1. Agents understand that “state is needed at the edge”, but do not know whether to model the system as “one DO per coordination unit” or mistakenly use a global singleton.
2. Correct DO writing practices are scattered across configuration, storage, concurrency, and testing; the idFromName() + fetch() skeleton from training data is no longer the official preferred approach.

The same repository also includes the general index Skill cloudflare and the more deployment-focused wrangler. When the task narrows down to “writing DOs / reviewing DOs / testing DOs”, you should use the durable-objects Skill.

The Skill directory follows the pattern of an entry point plus on-demand references:

skills/durable-objects/
├── SKILL.md
└── references/
    ├── rules.md      # Sharding, storage, concurrency, RPC, alarms, WebSockets
    ├── testing.md    # Vitest, unit/integration tests, alarm testing
    └── workers.md    # Worker caller side, types, wrangler, observability

The main text requires you to pull official pages before implementing features, rather than treating the reference files as the final API manual:

Resource URL
Documentation https://developers.cloudflare.com/durable-objects/
API https://developers.cloudflare.com/durable-objects/api/
Best Practices https://developers.cloudflare.com/durable-objects/best-practices/
Examples https://developers.cloudflare.com/durable-objects/examples/

Core Conventions

The Skill formalizes “when to use DOs” into a comparison table aligned with the official product page: each DO has a globally unique name, with storage and computation co-located, enabling coordination across multiple clients without building custom serialization and locks.

Use cases suitable for DOs:

Requirement Skill Examples
Coordination Chat rooms, multiplayer games, collaborative documents
Strong Consistency Inventory, bookings, turn-based matches
Entity-based Storage Multi-tenant SaaS, per-user data partitioning
Long-lived Connections WebSockets, real-time notifications
Entity-based Scheduling Subscription renewals, match timeouts

Explicit cases where DOs should NOT be used:
- Stateless request handling (use regular Workers)
- Workloads that need to be deployed globally rather than pinned to a single instance
- High-fanout, mutually independent requests

Core rules are documented in SKILL.md, with expanded details in references/rules.md:
1. Model by coordination atomic units: One DO per chat room, game session, or user; do not use a single global DO.
2. Use getByName() for deterministic routing: Route the same input to the same instance. Cloudflare officially added this method in the 2025-08-21 changelog, and the official getting started documentation now uses it, so you no longer need to first call idFromName() then get().
3. Use SQLite storage: Configure new_sqlite_classes in your migrations. The official changelog also states that new DO namespaces must use SQLite backends, and KV backend namespaces can no longer be created.
4. Initialize only in the constructor: Use blockConcurrencyWhile() only for creating tables or running schema migrations; do not wrap it around every request.
5. Use RPC methods instead of fetch() on DOs: Compatibility date >= 2024-04-03. This aligns with the Rules of Durable Objects.
6. Persist state before updating in-memory caches: Memory state will be lost if the instance is evicted or crashes, but SQLite storage remains intact.
7. Only one alarm per DO: setAlarm() will overwrite existing alarms.

The corresponding anti-patterns are clearly marked as NEVER allowed:
- A single global DO handling all requests (creates a bottleneck)
- Using blockConcurrencyWhile() for every request (kills throughput; reference files estimate ~5ms per call, with a maximum throughput of around 200 requests per second)
- Storing critical state only in memory
- Inserting await between related storage writes (breaks write batching and removes atomic commit guarantees)
- Performing fetch() or other external I/O inside blockConcurrencyWhile()

Installation and Enablement

The official README states that this Skill is designed for assistants that support the Agent Skills standard, including Claude Code, Cursor, OpenCode, OpenAI Codex, and Pi. Installation methods vary by tool and should not be mixed. The durable-objects Skill is listed in the repository’s Skills table alongside cloudflare, wrangler, and agents-sdk.

1. Using npx skills (cross-tool compatible)

Install the entire Cloudflare Skills collection:

npx skills add https://github.com/cloudflare/skills

To install only this single Skill, the command provided on the officialskills.sh page and skills.sh is:

npx skills add https://github.com/cloudflare/skills --skill durable-objects

2. Claude Code (Plugin Marketplace)

/plugin marketplace add cloudflare/skills
/plugin install cloudflare@cloudflare

3. Cursor
The repository README recommends installing from the Cursor Marketplace, or adding a remote rule via Settings > Rules > Add Rule > Remote Rule (Github) with the value cloudflare/skills. Cloudflare’s Cursor setup documentation also provides the slash command /add-plugin cloudflare, which installs the full Cloudflare Skills suite (including durable-objects) and registers the MCP. You can also search for the Skill by name in the Marketplace.

4. Clone and copy the directory manually

Tool Skill Directory Path
Claude Code ~/.claude/skills/
Cursor ~/.cursor/skills/
OpenCode ~/.config/opencode/skills/
OpenAI Codex ~/.codex/skills/
Pi ~/.pi/agent/skills/

You should copy the entire skills/durable-objects/ folder, preserving the relative path between SKILL.md and the references/ directory. Agents will generally enable the Skill automatically; it will trigger when chat rooms, bookings, DO bindings, alarms, or “help me review this Durable Object” appear in your prompts.

Typical Usage

The following examples are taken from the official SKILL.md and three reference files, demonstrating how the agent should behave after loading the Skill, rather than creating a separate tutorial.

1. Have the Agent follow the rules when modeling before writing code

You can directly include the trigger condition in your prompt:

Please help me build a room-isolated chat backend following the durable-objects Skill.
One DO per room, use getByName(roomId) for routing, store messages in SQLite, and use RPC to send messages.
Do not use a global singleton DO, and do not store critical state only in memory.
Before writing wrangler configuration and tests, consult the current Cloudflare Durable Objects documentation.

The Skill’s own listed trigger scenarios also include: performing best practice reviews for existing DOs, configuring bindings and migrations in wrangler.jsonc / wrangler.toml, writing tests with @cloudflare/vitest-pool-workers, and designing sharding and parent-child DO relationships.

2. Wrangler Bindings and SQLite Classes

The minimal configuration provided by the Skill entry point is:

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

A more complete example in references/workers.md will also include compatibility_date (RPC requires >= 2024-04-03) and multiple bindings. The equivalent wrangler.toml syntax uses [[durable_objects.bindings]] and [[migrations]].

There is one important point to follow the Skill’s “retrieval first” rule: the official Getting started guide now uses exports to declare DO classes and SQLite storage, and marks the old migrations array as legacy. The examples in the Skill repository still use new_sqlite_classes. When starting a new project, you should ask the agent to pull the latest official documentation instead of treating the fields in the reference files as the only correct answer.

Use the binding name to get a stub on the Worker side. The Skill recommends three creation methods:

// Deterministic routing, preferred for most scenarios
const stub = env.MY_DO.getByName("room-123");

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

// Create a new unique ID; you need to store the mapping externally
const id = env.MY_DO.newUniqueId();
const stub = env.MY_DO.get(id);

For latency-sensitive scenarios, references/rules.md also mentions that you can pass a locationHint when creating, such as wnam, enam, weur, or apac. Refer to the official API documentation for specific parameter values.

3. Basic SQLite + RPC Skeleton

The minimal runnable pattern provided by the Skill is as follows. The class inherits from DurableObject from cloudflare:workers; use blockConcurrencyWhile() in the constructor to create tables; only expose RPC methods publicly.

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 });
  },
};

Several points are repeatedly emphasized in the reference files:
- The SQL API is synchronous: use this.ctx.storage.sql.exec(...), use .toArray() for multi-row reads, and .one() for single-row reads.
- KV-style storage.put / storage.get are still available but asynchronous; new code should prioritize SQL.
- Do not use await between related writes, to allow the runtime to batch them into a single atomic commit. For example, three sql.exec calls for a transfer (debit, credit, and transaction log) should be written consecutively.
- The Worker’s fetch handler can be retained as an HTTP entry point; business methods on the DO class should use RPC. The official getting started tutorial’s sayHello() uses the same pattern.

The chat room scenario in rules.md is written as “one stub per room”:

const stub = env.CHAT_ROOM.getByName(roomId);
const msg = await stub.sendMessage("user-123", "Hello!");

For hierarchical scenarios, the parent DO only stores references, while the child DO manages its own state. For example, GameServer.createMatch() inserts a matchId into its own table, then calls this.env.GAME_MATCH.getByName(matchId) to initialize the child object.

Do not use PRAGMA user_version for schema evolution, as DO SQLite does not support it. The reference file recommends creating a _sql_schema_migrations table and running migrations incrementally by version in the constructor’s blockConcurrencyWhile(). For production projects, you can refer to durable-utils or similar tools in Cloudflare Actors, which are explicitly referenced in the Skill’s original text and not included as built-in code for this Skill.

4. Alarms, WebSockets, and Testing

Each DO can only have one active alarm, suitable for scenarios like “wake up when this room/tenant expires”:

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

async alarm(): Promise<void> {
  // Handle expiration tasks; set another alarm if needed for follow-up work
}

await this.ctx.storage.deleteAlarm();

Failures will automatically retry, so your handler must be idempotent. WebSockets use the Hibernation API: this.ctx.acceptWebSocket(...), then implement webSocketMessage / webSocketClose, and broadcast messages by iterating over getWebSockets().

Testing uses @cloudflare/vitest-pool-workers, running in the Workers runtime instead of mocking DOs with regular Node tests. The installation command listed in the Skill’s testing.md is:

npm i -D vitest@~3.2.0 @cloudflare/vitest-pool-workers

Always refer to the current version of the reference file and npm for the latest version numbers, as this is not a permanent lock. A minimal test case is as follows, directly calling RPC on the stub:

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);
  });
});

The same reference also covers: using SELF.fetch for HTTP integration tests, using runInDurableObject() to inspect internal storage of an instance, using runDurableObjectAlarm() to trigger an alarm immediately, and using listDurableObjectIds() to list IDs in the namespace. Storage for each test is isolated, so DOs created in one test case will not leak into the next. Run tests with:

npx vitest        # Watch mode
npx vitest run    # Single run

Applicable Scenarios and Notes

Cases where this Skill is particularly suitable:
1. You need to build chat rooms, multiplayer collaboration, booking/inventory, tenant-based SQLite storage, or shared-state WebSockets on Workers.
2. You have existing DO code and want to review sharding, concurrency, and persistence issues according to official rules.
3. You need to write Wrangler bindings, RPC methods, and Vitest