Generated banner for Durable Objects
Generated logo for Durable Objects

Durable Objects

CloudflareOfficialPublished by Cloudflare

  • Skill
  • Tool integration
  • Cloud and DevOps
  • Apache-2.0
  • No issues found

From the author

Build, debug, or review Cloudflare Durable Objects code for persistent state and coordination.
— the description by Cloudflare

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides guidance on implementing Cloudflare Durable Objects, which enable stateful coordination on the edge. It covers the use of Durable Objects for tasks requiring strong consistency, persistent connections, and per-entity storage. The skill includes best practices for configuration, storage operations using SQLite or KV, alarm scheduling, and testing strategies, while emphasizing the importance of using current documentation for API implementation.

What it does

  • Provides patterns for creating and configuring Durable Object classes.
  • Offers guidance on using SQLite and KV storage within Durable Objects.
  • Assists in setting up alarms for scheduled per-entity work.
  • Helps with configuring bindings and migrations in wrangler files.
  • Supports the implementation of RPC methods and WebSocket handlers.
  • Provides references for testing Durable Objects using Vitest.

When to use it

  • When building stateful coordination features like chat rooms or multiplayer games.
  • When implementing systems requiring strong consistency, such as inventory management.
  • When managing per-user data or multi-tenant application state.
  • When handling persistent connections like WebSockets.
  • When reviewing existing Durable Object code for alignment with best practices.

Advantages

  • Promotes the use of current, authoritative documentation via retrieval.
  • Provides clear patterns for deterministic routing and storage initialization.
  • Helps avoid common anti-patterns like blocking concurrency on every request.
  • Supports structured configuration for migrations and bindings.

Usage examples

“How do I initialize a SQLite database in a Durable Object?”
The agent provides a pattern using blockConcurrencyWhile in the constructor to execute SQL schema creation.
“What is the best way to route requests to a specific Durable Object instance?”
The agent explains how to use getByName for deterministic routing to ensure the same input maps to the same object instance.
“How should I handle scheduled tasks within a Durable Object?”
The agent demonstrates the use of setAlarm and the alarm() handler method for per-object scheduled work.
“Can you review my Durable Object migration configuration?”
The agent checks the wrangler configuration against the provided patterns for bindings and new_sqlite_classes.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/cloudflare/durable-objects/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip durable-objects.zip -d .agents/skills

Documentation by Cloudflare

Durable Objects

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

Retrieval Sources

Your knowledge of Durable Objects APIs and configuration may be outdated. Prefer retrieval over pre-training for any Durable Objects task.

Fetch the relevant doc page when implementing features.

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’s Vitest integration

  • Designing sharding strategies and parent-child relationships

Reference Documentation

  • ./references/rules.md - Core rules, storage, concurrency, RPC, alarms

  • Testing reference - Current Vitest documentation, migration choices, and test selection

  • Workers reference - Workers handlers, types, configuration, tracing, and local debugging

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

Core Principles

Use Durable Objects For
NeedExample
CoordinationChat rooms, multiplayer games, collaborative docs
Strong consistencyInventory, booking systems, turn-based games
Per-entity storageMulti-tenant SaaS, per-user data
Persistent connectionsWebSockets, real-time notifications
Scheduled work per entitySubscription 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
// wrangler.jsonc
{
  "durable_objects": {
    "bindings": [{ "name": "MY_DO", "class_name": "MyDurableObject" }]
  },
  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyDurableObject"] }]
}
Basic Durable Object Pattern
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

Authorization

Durable Objects do not have separate roles or permissions; access follows the Worker that implements them. Retrieve the current Durable Objects authorization guidance before granting observability or Data Studio access, and scope the Workers role to the intended Worker or Workers product.

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

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

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

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

Read the testing reference before configuring a suite or writing Durable Object tests. It routes to current setup, APIs, and examples and identifies the behavior to cover.

Skill feedback

If the user reports that this skill gave incorrect or outdated guidance, or is missing information, read references/skill-feedback.md.

Files

references/workers.md

Cloudflare Workers Best Practices

High-level guidance for Workers that invoke Durable Objects.

Wrangler Configuration

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "my-worker",
  "main": "src/index.ts",
  "compatibility_date": "2024-12-01",
  "compatibility_flags": ["nodejs_compat"],

  "durable_objects": {
    "bindings": [
      { "name": "CHAT_ROOM", "class_name": "ChatRoom" },
      { "name": "USER_SESSION", "class_name": "UserSession" }
    ]
  },

  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["ChatRoom", "UserSession"] }
  ],

  // Environment variables
  "vars": {
    "ENVIRONMENT": "production"
  },

  // KV namespaces
  "kv_namespaces": [
    { "binding": "CONFIG", "id": "abc123" }
  ],

  // R2 buckets
  "r2_buckets": [
    { "binding": "UPLOADS", "bucket_name": "my-uploads" }
  ],

  // D1 databases
  "d1_databases": [
    { "binding": "DB", "database_id": "xyz789" }
  ]
}
wrangler.toml (Alternative)
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-12-01"
compatibility_flags = ["nodejs_compat"]

[[durable_objects.bindings]]
name = "CHAT_ROOM"
class_name = "ChatRoom"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["ChatRoom"]

[vars]
ENVIRONMENT = "production"

TypeScript Types

Environment Interface
// src/types.ts
import { ChatRoom } from "./durable-objects/chat-room";
import { UserSession } from "./durable-objects/user-session";

export interface Env {
  // Durable Objects
  CHAT_ROOM: DurableObjectNamespace<ChatRoom>;
  USER_SESSION: DurableObjectNamespace<UserSession>;

  // KV
  CONFIG: KVNamespace;

  // R2
  UPLOADS: R2Bucket;

  // D1
  DB: D1Database;

  // Environment variables
  ENVIRONMENT: string;
  API_KEY: string; // From secrets
}
Export Durable Object Classes
// src/index.ts
export { ChatRoom } from "./durable-objects/chat-room";
export { UserSession } from "./durable-objects/user-session";

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    // Worker handler
  },
};

Worker Handler Pattern

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);

    try {
      // Route to appropriate handler
      if (url.pathname.startsWith("/api/rooms")) {
        return handleRooms(request, env);
      }
      if (url.pathname.startsWith("/api/users")) {
        return handleUsers(request, env);
      }

      return new Response("Not Found", { status: 404 });
    } catch (error) {
      console.error("Request failed:", error);
      return new Response("Internal Server Error", { status: 500 });
    }
  },
};

async function handleRooms(request: Request, env: Env): Promise<Response> {
  const url = new URL(request.url);
  const roomId = url.searchParams.get("room");

  if (!roomId) {
    return Response.json({ error: "Missing room parameter" }, { status: 400 });
  }

  const stub = env.CHAT_ROOM.getByName(roomId);

  if (request.method === "POST") {
    const body = await request.json<{ userId: string; message: string }>();
    const result = await stub.sendMessage(body.userId, body.message);
    return Response.json(result);
  }

  const messages = await stub.getMessages();
  return Response.json(messages);
}

Request Validation

import { z } from "zod";

const SendMessageSchema = z.object({
  userId: z.string().min(1),
  message: z.string().min(1).max(1000),
});

async function handleSendMessage(request: Request, env: Env): Promise<Response> {
  const body = await request.json();
  const result = SendMessageSchema.safeParse(body);

  if (!result.success) {
    return Response.json(
      { error: "Validation failed", details: result.error.issues },
      { status: 400 }
    );
  }

  const stub = env.CHAT_ROOM.getByName(result.data.userId);
  const message = await stub.sendMessage(result.data.userId, result.data.message);
  return Response.json(message);
}

Observability & Logging

Use Workers Logs for structured application logs and pass objects to console.log() so their fields can be queried.

Workers Traces

Use Workers Traces for automatic instrumentation of handlers, binding operations, and RPC calls. Enable observability.traces.enabled and choose observability.traces.head_sampling_rate explicitly for the workload or investigation. Enabling logs alone does not enable traces.

Local Debugging

Start the project's dev server with cf dev where supported, or its Wrangler or Vite command. Local Explorer captures local traces and logs. See its docs for supported versions.

Use the Local Explorer API URL printed at startup:

  • GET /cdn-cgi/local/explorer/api returns the OpenAPI spec.

  • POST /cdn-cgi/local/explorer/api/local/observability/query queries traces and logs with SQL. Use the spec for request and response formats.

Timing Limits

In production, Date.now() and performance.now() only advance after I/O, so they cannot time synchronous SQL or CPU work. Local timers advance during execution. See performance and timers.

Non-I/O spans can report 0 ms even when the operation takes longer. See trace limitations before using span durations.

Error Handling

Graceful DO Errors
async function callDO(stub: DurableObjectStub<ChatRoom>, method: string): Promise<Response> {
  try {
    const result = await stub.getMessages();
    return Response.json(result);
  } catch (error) {
    if (error instanceof Error) {
      // DO threw an error
      console.error({ message: "DO operation failed", error: error.message });
      return Response.json(
        { error: "Service temporarily unavailable" },
        { status: 503 }
      );
    }
    throw error;
  }
}
Timeout Handling
async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  const timeout = new Promise<never>((_, reject) =>
    setTimeout(() => reject(new Error("Timeout")), ms)
  );
  return Promise.race([promise, timeout]);
}

// Usage
const result = await withTimeout(stub.processData(data), 5000);

CORS Handling

function corsHeaders(): HeadersInit {
  return {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type, Authorization",
  };
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method === "OPTIONS") {
      return new Response(null, { headers: corsHeaders() });
    }

    const response = await handleRooms(request, env);
    
    // Add CORS headers to response
    const newHeaders = new Headers(response.headers);
    Object.entries(corsHeaders()).forEach(([k, v]) => newHeaders.set(k, v));
    
    return new Response(response.body, {
      status: response.status,
      headers: newHeaders,
    });
  },
};

Secrets Management

Set secrets via wrangler CLI (not in config files):

wrangler secret put API_KEY
wrangler secret put DATABASE_URL

Access in code:

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const apiKey = env.API_KEY; // From secret
    // ...
  },
};

Development Commands

# Local development
wrangler dev

# Deploy
wrangler deploy

# Tail logs
wrangler tail

# List DOs
wrangler d1 execute DB --command "SELECT * FROM _cf_DO"

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Cloudflare
Publisher
Cloudflare (Official)
Source
cloudflare/skills · skills/durable-objects · commit 6eb6c78
Licence
Apache License 2.0 (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Cloudflare or Cloudflare.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download durable-objects.zip