---
title: Rules of Durable Objects
description: Design guidelines for building correct and effective Durable Objects applications, covering when and how to use them.
image: https://developers.cloudflare.com/dev-products-preview.png
---

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/durable-objects/llms.txt  
> Use this file to discover all available pages before exploring further.

[Skip to content](#%5Ftop) 

# Rules of Durable Objects

Durable Objects provide a powerful primitive for building stateful, coordinated applications. Each Durable Object is a single-threaded, globally-unique instance with its own persistent storage. Understanding how to design around these properties is essential for building effective applications.

This is a guidebook on how to build more effective and correct Durable Object applications.

## When to use Durable Objects

### Use Durable Objects for stateful coordination, not stateless request handling

Workers are stateless functions: each request may run on a different instance, in a different location, with no shared memory between requests. Durable Objects are stateful compute: each instance has a unique identity, runs in a single location, and maintains state across requests.

Use Durable Objects when you need:

* **Coordination** — Multiple clients need to interact with shared state (chat rooms, multiplayer games, collaborative documents)
* **Strong consistency** — Operations must be serialized to avoid race conditions (inventory management, booking systems, turn-based games)
* **Per-entity storage** — Each user, tenant, or resource needs its own isolated database (multi-tenant SaaS, per-user data)
* **Persistent connections** — Long-lived WebSocket connections that survive across requests (real-time notifications, live updates)
* **Scheduled work per entity** — Each entity needs its own timer or scheduled task (subscription renewals, game timeouts)

Use plain Workers when you need:

* **Stateless request handling** — API endpoints, proxies, or transformations with no shared state
* **Maximum global distribution** — Requests should be handled at the nearest edge location
* **High fan-out** — Each request is independent and can be processed in parallel

* [  JavaScript ](#tab-panel-5985)
* [  TypeScript ](#tab-panel-5986)

index.js

```

import { DurableObject } from "cloudflare:workers";


// ✅ Good use of Durable Objects: Seat booking requires coordination

// All booking requests for a venue must be serialized to prevent double-booking

export class SeatBooking extends DurableObject {

  async bookSeat(seatId, userId) {

    // Check if seat is already booked

    const existing = this.ctx.storage.sql

      .exec("SELECT user_id FROM bookings WHERE seat_id = ?", seatId)

      .toArray();


    if (existing.length > 0) {

      return { success: false, message: "Seat already booked" };

    }


    // Book the seat - this is safe because Durable Objects are single-threaded

    this.ctx.storage.sql.exec(

      "INSERT INTO bookings (seat_id, user_id, booked_at) VALUES (?, ?, ?)",

      seatId,

      userId,

      Date.now(),

    );


    return { success: true, message: "Seat booked successfully" };

  }

}


export default {

  async fetch(request, env) {

    const url = new URL(request.url);

    const eventId = url.searchParams.get("event") ?? "default";


    // Route to a Durable Object by event ID

    // All bookings for the same event go to the same instance

    const id = env.BOOKING.idFromName(eventId);

    const booking = env.BOOKING.get(id);


    const { seatId, userId } = await request.json();

    const result = await booking.bookSeat(seatId, userId);


    return Response.json(result, {

      status: result.success ? 200 : 409,

    });

  },

};


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  BOOKING: DurableObjectNamespace<SeatBooking>;

}


// ✅ Good use of Durable Objects: Seat booking requires coordination

// All booking requests for a venue must be serialized to prevent double-booking

export class SeatBooking extends DurableObject<Env> {

async bookSeat(

seatId: string,

userId: string

): Promise<{ success: boolean; message: string }> {

// Check if seat is already booked

const existing = this.ctx.storage.sql

.exec<{ user_id: string }>(

"SELECT user_id FROM bookings WHERE seat_id = ?",

seatId

)

.toArray();


      if (existing.length > 0) {

        return { success: false, message: "Seat already booked" };

      }


      // Book the seat - this is safe because Durable Objects are single-threaded

      this.ctx.storage.sql.exec(

        "INSERT INTO bookings (seat_id, user_id, booked_at) VALUES (?, ?, ?)",

        seatId,

        userId,

        Date.now()

      );


      return { success: true, message: "Seat booked successfully" };

    }

}


export default {

  async fetch(request: Request, env: Env): Promise<Response> {

    const url = new URL(request.url);

    const eventId = url.searchParams.get("event") ?? "default";


      // Route to a Durable Object by event ID

      // All bookings for the same event go to the same instance

      const id = env.BOOKING.idFromName(eventId);

      const booking = env.BOOKING.get(id);


      const { seatId, userId } = await request.json<{

        seatId: string;

        userId: string;

      }>();

      const result = await booking.bookSeat(seatId, userId);


      return Response.json(result, {

        status: result.success ? 200 : 409,

      });

    },

};


```

A common pattern is to use Workers as the stateless entry point that routes requests to Durable Objects when coordination is needed. The Worker handles authentication, validation, and response formatting, while the Durable Object handles the stateful logic.

## Design and sharding

### Model your Durable Objects around your "atom" of coordination

The most important design decision is choosing what each Durable Object represents. Create one Durable Object per logical unit that needs coordination: a chat room, a game session, a document, a user's data, or a tenant's workspace.

This is the key insight that makes Durable Objects powerful. Instead of a shared database with locks, each "atom" of your application gets its own single-threaded execution environment with private storage.

* [  JavaScript ](#tab-panel-5973)
* [  TypeScript ](#tab-panel-5974)

index.js

```

import { DurableObject } from "cloudflare:workers";


// Each chat room is its own Durable Object instance

export class ChatRoom extends DurableObject {

  async sendMessage(userId, message) {

    // All messages to this room are processed sequentially by this single instance.

    // No race conditions, no distributed locks needed.

    this.ctx.storage.sql.exec(

      "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)",

      userId,

      message,

      Date.now(),

    );

  }

}


export default {

  async fetch(request, env) {

    const url = new URL(request.url);

    const roomId = url.searchParams.get("room") ?? "lobby";


    // Each room ID maps to exactly one Durable Object instance globally

    const id = env.CHAT_ROOM.idFromName(roomId);

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


    await stub.sendMessage("user-123", "Hello, room!");

    return new Response("Message sent");

  },

};


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  CHAT_ROOM: DurableObjectNamespace<ChatRoom>;

}


// Each chat room is its own Durable Object instance

export class ChatRoom extends DurableObject<Env> {

  async sendMessage(userId: string, message: string) {

    // All messages to this room are processed sequentially by this single instance.

    // No race conditions, no distributed locks needed.

    this.ctx.storage.sql.exec(

      "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)",

      userId,

      message,

      Date.now()

    );

  }

}


export default {

  async fetch(request: Request, env: Env): Promise<Response> {

    const url = new URL(request.url);

    const roomId = url.searchParams.get("room") ?? "lobby";


    // Each room ID maps to exactly one Durable Object instance globally

    const id = env.CHAT_ROOM.idFromName(roomId);

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


    await stub.sendMessage("user-123", "Hello, room!");

    return new Response("Message sent");

  },

};


```

Note

If you have global application or user configuration that you need to access frequently (on every request), consider using [Workers KV](https://developers.cloudflare.com/kv/) instead.

Do not create a single "global" Durable Object that handles all requests:

* [  JavaScript ](#tab-panel-5971)
* [  TypeScript ](#tab-panel-5972)

index.js

```

import { DurableObject } from "cloudflare:workers";


// 🔴 Bad: A single Durable Object handling ALL chat rooms

export class ChatRoom extends DurableObject {

  async sendMessage(roomId, userId, message) {

    // All messages for ALL rooms go through this single instance.

    // This becomes a bottleneck as traffic grows.

    this.ctx.storage.sql.exec(

      "INSERT INTO messages (room_id, user_id, content) VALUES (?, ?, ?)",

      roomId,

      userId,

      message,

    );

  }

}


export default {

  async fetch(request, env) {

    // 🔴 Bad: Always using the same ID means one global instance

    const id = env.CHAT_ROOM.idFromName("global");

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


    await stub.sendMessage("room-123", "user-456", "Hello!");

    return new Response("Sent");

  },

};


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  CHAT_ROOM: DurableObjectNamespace<ChatRoom>;

}


// 🔴 Bad: A single Durable Object handling ALL chat rooms

export class ChatRoom extends DurableObject<Env> {

async sendMessage(roomId: string, userId: string, message: string) {

// All messages for ALL rooms go through this single instance.

// This becomes a bottleneck as traffic grows.

this.ctx.storage.sql.exec(

"INSERT INTO messages (room_id, user_id, content) VALUES (?, ?, ?)",

roomId,

userId,

message

);

}

}


export default {

  async fetch(request: Request, env: Env): Promise<Response> {

    // 🔴 Bad: Always using the same ID means one global instance

    const id = env.CHAT_ROOM.idFromName("global");

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


      await stub.sendMessage("room-123", "user-456", "Hello!");

      return new Response("Sent");

    },

};


```

### Message throughput limits

A single Durable Object can handle approximately **500-1,000 requests per second** for simple operations. This limit varies based on the work performed per request:

| Operation type                                      | Throughput        |
| --------------------------------------------------- | ----------------- |
| Simple pass-through (minimal parsing)               | \~1,000 req/sec   |
| Moderate processing (JSON parsing, validation)      | \~500-750 req/sec |
| Complex operations (transformation, storage writes) | \~200-500 req/sec |

When modeling your "atom," factor in the expected request rate. If your use case exceeds these limits, shard your workload across multiple Durable Objects.

For example, consider a real-time game with 50,000 concurrent players sending 10 updates per second. This generates 500,000 requests per second total. You would need 500-1,000 game session Durable Objects—not one global coordinator.

Calculate your sharding requirements:

```

Required DOs = (Total requests/second) / (Requests per DO capacity)


```

### Use deterministic IDs for predictable routing

Use `getByName()` with meaningful, deterministic strings for consistent routing. The same input always produces the same Durable Object ID, ensuring requests for the same logical entity always reach the same instance.

* [  JavaScript ](#tab-panel-5975)
* [  TypeScript ](#tab-panel-5976)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class GameSession extends DurableObject {

  async join(playerId) {

    // Game logic here

  }

}


export default {

  async fetch(request, env) {

    const url = new URL(request.url);

    const gameId = url.searchParams.get("game");


    if (!gameId) {

      return new Response("Missing game ID", { status: 400 });

    }


    // ✅ Good: Deterministic ID from a meaningful string

    // All requests for "game-abc123" go to the same Durable Object

    const stub = env.GAME_SESSION.getByName(gameId);


    await stub.join("player-xyz");

    return new Response("Joined game");

  },

};


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  GAME_SESSION: DurableObjectNamespace<GameSession>;

}


export class GameSession extends DurableObject<Env> {

  async join(playerId: string) {

    // Game logic here

  }

}


export default {

  async fetch(request: Request, env: Env): Promise<Response> {

    const url = new URL(request.url);

    const gameId = url.searchParams.get("game");


    if (!gameId) {

      return new Response("Missing game ID", { status: 400 });

    }


    // ✅ Good: Deterministic ID from a meaningful string

    // All requests for "game-abc123" go to the same Durable Object

    const stub = env.GAME_SESSION.getByName(gameId);


    await stub.join("player-xyz");

    return new Response("Joined game");

  },

};


```

Creating a stub does not instantiate or wake up the Durable Object. The Durable Object is only activated when you call a method on the stub.

Use `newUniqueId()` only when you need a new, random instance and will store the mapping externally:

* [  JavaScript ](#tab-panel-5969)
* [  TypeScript ](#tab-panel-5970)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class GameSession extends DurableObject {

  async join(playerId) {

    // Game logic here

  }

}


export default {

  async fetch(request, env) {

    // newUniqueId() creates a random ID - useful when creating new instances

    // You must store this ID somewhere (e.g., D1) to find it again later

    const id = env.GAME_SESSION.newUniqueId();

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


    // Store the mapping: gameCode -> id.toString()

    // await env.DB.prepare("INSERT INTO games (code, do_id) VALUES (?, ?)").bind(gameCode, id.toString()).run();


    return Response.json({ gameId: id.toString() });

  },

};


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  GAME_SESSION: DurableObjectNamespace<GameSession>;

}


export class GameSession extends DurableObject<Env> {

  async join(playerId: string) {

    // Game logic here

  }

}


export default {

  async fetch(request: Request, env: Env): Promise<Response> {

    // newUniqueId() creates a random ID - useful when creating new instances

    // You must store this ID somewhere (e.g., D1) to find it again later

    const id = env.GAME_SESSION.newUniqueId();

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


      // Store the mapping: gameCode -> id.toString()

      // await env.DB.prepare("INSERT INTO games (code, do_id) VALUES (?, ?)").bind(gameCode, id.toString()).run();


      return Response.json({ gameId: id.toString() });

    },

};


```

### Use parent-child relationships for related entities

Do not put all your data in a single Durable Object. When you have hierarchical data (workspaces containing projects, game servers managing matches), create separate child Durable Objects for each entity. The parent coordinates and tracks children, while children handle their own state independently.

This enables parallelism: operations on different children can happen concurrently, while each child maintains its own single-threaded consistency ([read more about this pattern](https://developers.cloudflare.com/reference-architecture/diagrams/storage/durable-object-control-data-plane-pattern/)).

* [  JavaScript ](#tab-panel-6003)
* [  TypeScript ](#tab-panel-6004)

index.js

```

import { DurableObject } from "cloudflare:workers";


// Parent: Coordinates matches, but doesn't store match data

export class GameServer extends DurableObject {

  async createMatch(matchName) {

    const matchId = crypto.randomUUID();


    // Store reference to the child in parent's database

    this.ctx.storage.sql.exec(

      "INSERT INTO matches (id, name, created_at) VALUES (?, ?, ?)",

      matchId,

      matchName,

      Date.now(),

    );


    // Initialize the child Durable Object

    const childId = this.env.GAME_MATCH.idFromName(matchId);

    const childStub = this.env.GAME_MATCH.get(childId);

    await childStub.init(matchId, matchName);


    return matchId;

  }


  async listMatches() {

    // Parent knows about all matches without waking up each child

    const cursor = this.ctx.storage.sql.exec(

      "SELECT id, name FROM matches ORDER BY created_at DESC",

    );

    return cursor.toArray();

  }

}


// Child: Handles its own game state independently

export class GameMatch extends DurableObject {

  async init(matchId, matchName) {

    await this.ctx.storage.put("matchId", matchId);

    await this.ctx.storage.put("matchName", matchName);

    this.ctx.storage.sql.exec(`

      CREATE TABLE IF NOT EXISTS players (

        id TEXT PRIMARY KEY,

        name TEXT NOT NULL,

        score INTEGER DEFAULT 0

      )

    `);

  }


  async addPlayer(playerId, playerName) {

    this.ctx.storage.sql.exec(

      "INSERT INTO players (id, name, score) VALUES (?, ?, 0)",

      playerId,

      playerName,

    );

  }


  async updateScore(playerId, score) {

    this.ctx.storage.sql.exec(

      "UPDATE players SET score = ? WHERE id = ?",

      score,

      playerId,

    );

  }

}


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  GAME_SERVER: DurableObjectNamespace<GameServer>;

  GAME_MATCH: DurableObjectNamespace<GameMatch>;

}


// Parent: Coordinates matches, but doesn't store match data

export class GameServer extends DurableObject<Env> {

  async createMatch(matchName: string): Promise<string> {

    const matchId = crypto.randomUUID();


    // Store reference to the child in parent's database

    this.ctx.storage.sql.exec(

      "INSERT INTO matches (id, name, created_at) VALUES (?, ?, ?)",

      matchId,

      matchName,

      Date.now()

    );


    // Initialize the child Durable Object

    const childId = this.env.GAME_MATCH.idFromName(matchId);

    const childStub = this.env.GAME_MATCH.get(childId);

    await childStub.init(matchId, matchName);


    return matchId;

  }


  async listMatches(): Promise<{ id: string; name: string }[]> {

    // Parent knows about all matches without waking up each child

    const cursor = this.ctx.storage.sql.exec<{ id: string; name: string }>(

      "SELECT id, name FROM matches ORDER BY created_at DESC"

    );

    return cursor.toArray();

  }

}


// Child: Handles its own game state independently

export class GameMatch extends DurableObject<Env> {

  async init(matchId: string, matchName: string) {

    await this.ctx.storage.put("matchId", matchId);

    await this.ctx.storage.put("matchName", matchName);

    this.ctx.storage.sql.exec(`

      CREATE TABLE IF NOT EXISTS players (

        id TEXT PRIMARY KEY,

        name TEXT NOT NULL,

        score INTEGER DEFAULT 0

      )

    `);

  }


  async addPlayer(playerId: string, playerName: string) {

    this.ctx.storage.sql.exec(

      "INSERT INTO players (id, name, score) VALUES (?, ?, 0)",

      playerId,

      playerName

    );

  }


  async updateScore(playerId: string, score: number) {

    this.ctx.storage.sql.exec(

      "UPDATE players SET score = ? WHERE id = ?",

      score,

      playerId

    );

  }

}


```

With this pattern:

* Listing matches only queries the parent (children stay hibernated)
* Different matches process player actions in parallel
* Each match has its own SQLite database for player data

### Consider location hints for latency-sensitive applications

By default, a Durable Object is created near the location of the first request it receives. For most applications, this works well. However, you can provide a location hint to influence where the Durable Object is created.

* [  JavaScript ](#tab-panel-5977)
* [  TypeScript ](#tab-panel-5978)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class GameSession extends DurableObject {

  // Game session logic

}


export default {

  async fetch(request, env) {

    const url = new URL(request.url);

    const gameId = url.searchParams.get("game") ?? "default";

    const region = url.searchParams.get("region") ?? "wnam"; // Western North America


    // Provide a location hint for where this Durable Object should be created

    const id = env.GAME_SESSION.idFromName(gameId);

    const stub = env.GAME_SESSION.get(id, { locationHint: region });


    return new Response("Connected to game session");

  },

};


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  GAME_SESSION: DurableObjectNamespace<GameSession>;

}


export class GameSession extends DurableObject<Env> {

  // Game session logic

}


export default {

  async fetch(request: Request, env: Env): Promise<Response> {

    const url = new URL(request.url);

    const gameId = url.searchParams.get("game") ?? "default";

    const region = url.searchParams.get("region") ?? "wnam"; // Western North America


      // Provide a location hint for where this Durable Object should be created

      const id = env.GAME_SESSION.idFromName(gameId);

      const stub = env.GAME_SESSION.get(id, { locationHint: region });


      return new Response("Connected to game session");

    },

};


```

Location hints are suggestions, not guarantees. Refer to [Data location](https://developers.cloudflare.com/durable-objects/reference/data-location/) for available regions and details.

## Storage and state

### Use SQLite-backed Durable Objects

[SQLite storage](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/) is the recommended storage backend for new Durable Objects. It provides a familiar SQL API for relational queries, indexes, transactions, and better performance than the legacy key-value storage backed Durable Objects. SQLite Durable Objects also support the KV API in synchronous and asynchronous versions.

Configure your Durable Object class to use SQLite storage in your Wrangler configuration:

* [  wrangler.jsonc ](#tab-panel-5965)
* [  wrangler.toml ](#tab-panel-5966)

JSONC

```

{

  "migrations": [

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

  ]

}


```

TOML

```

[[migrations]]

tag = "v1"

new_sqlite_classes = [ "ChatRoom" ]


```

Then use the SQL API in your Durable Object:

* [  JavaScript ](#tab-panel-5987)
* [  TypeScript ](#tab-panel-5988)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class ChatRoom extends DurableObject {

  constructor(ctx, env) {

    super(ctx, env);


    // Create tables on first instantiation

    this.ctx.storage.sql.exec(`

        CREATE TABLE IF NOT EXISTS messages (

          id INTEGER PRIMARY KEY AUTOINCREMENT,

          user_id TEXT NOT NULL,

          content TEXT NOT NULL,

          created_at INTEGER NOT NULL

        )

      `);

  }


  async addMessage(userId, content) {

    this.ctx.storage.sql.exec(

      "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)",

      userId,

      content,

      Date.now(),

    );

  }


  async getRecentMessages(limit = 50) {

    // Use type parameter for typed results

    const cursor = this.ctx.storage.sql.exec(

      "SELECT * FROM messages ORDER BY created_at DESC LIMIT ?",

      limit,

    );

    return cursor.toArray();

  }

}


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  CHAT_ROOM: DurableObjectNamespace<ChatRoom>;

}


type Message = {

id: number;

user_id: string;

content: string;

created_at: number;

};


export class ChatRoom extends DurableObject<Env> {

  constructor(ctx: DurableObjectState, env: Env) {

    super(ctx, env);


      // Create tables on first instantiation

      this.ctx.storage.sql.exec(`

        CREATE TABLE IF NOT EXISTS messages (

          id INTEGER PRIMARY KEY AUTOINCREMENT,

          user_id TEXT NOT NULL,

          content TEXT NOT NULL,

          created_at INTEGER NOT NULL

        )

      `);

    }


    async addMessage(userId: string, content: string) {

      this.ctx.storage.sql.exec(

        "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)",

        userId,

        content,

        Date.now()

      );

    }


    async getRecentMessages(limit: number = 50): Promise<Message[]> {

      // Use type parameter for typed results

      const cursor = this.ctx.storage.sql.exec<Message>(

        "SELECT * FROM messages ORDER BY created_at DESC LIMIT ?",

        limit

      );

      return cursor.toArray();

    }

}


```

Refer to [Access Durable Objects storage](https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage/) for more details on the SQL API.

### Initialize storage and run migrations in the constructor

Use `blockConcurrencyWhile()` in the constructor to run migrations and initialize state before any requests are processed. This ensures your schema is ready and prevents race conditions during initialization.

Note

`PRAGMA user_version` is not supported by Durable Objects SQLite storage. You must use an alternative approach to track your schema version.

For production applications, use a migration library that handles version tracking and execution automatically:

* [durable-utils ↗](https://github.com/lambrospetrou/durable-utils#sqlite-schema-migrations) — provides a `SQLSchemaMigrations` class that tracks executed migrations both in memory and in storage.
* [@cloudflare/actors storage utilities ↗](https://github.com/cloudflare/actors/blob/main/packages/storage/src/sql-schema-migrations.ts) — a reference implementation of the same pattern used by the Cloudflare Actors framework.

If you prefer not to use a library, you can track schema versions manually using a `_sql_schema_migrations` table. The following example demonstrates this approach:

* [  JavaScript ](#tab-panel-5999)
* [  TypeScript ](#tab-panel-6000)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class ChatRoom extends DurableObject {

  constructor(ctx, env) {

    super(ctx, env);


    // blockConcurrencyWhile() ensures no requests are processed until this completes

    ctx.blockConcurrencyWhile(async () => {

      await this.migrate();

    });

  }


  async migrate() {

    // Create the migrations tracking table if it does not exist

    this.ctx.storage.sql.exec(`

      CREATE TABLE IF NOT EXISTS _sql_schema_migrations (

        id INTEGER PRIMARY KEY,

        applied_at TEXT NOT NULL DEFAULT (datetime('now'))

      );

    `);


    // Determine the current schema version

    const version = this.ctx.storage.sql

      .exec(

        "SELECT COALESCE(MAX(id), 0) as version FROM _sql_schema_migrations",

      )

      .one().version;


    if (version < 1) {

      this.ctx.storage.sql.exec(`

        CREATE TABLE IF NOT EXISTS messages (

          id INTEGER PRIMARY KEY AUTOINCREMENT,

          user_id TEXT NOT NULL,

          content TEXT NOT NULL,

          created_at INTEGER NOT NULL

        );

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

        INSERT INTO _sql_schema_migrations (id) VALUES (1);

      `);

    }


    if (version < 2) {

      // Future migration: add a new column

      this.ctx.storage.sql.exec(`

        ALTER TABLE messages ADD COLUMN edited_at INTEGER;

        INSERT INTO _sql_schema_migrations (id) VALUES (2);

      `);

    }

  }

}


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  CHAT_ROOM: DurableObjectNamespace<ChatRoom>;

}


export class ChatRoom extends DurableObject<Env> {

  constructor(ctx: DurableObjectState, env: Env) {

    super(ctx, env);


    // blockConcurrencyWhile() ensures no requests are processed until this completes

    ctx.blockConcurrencyWhile(async () => {

      await this.migrate();

    });

  }


  private async migrate() {

    // Create the migrations tracking table if it does not exist

    this.ctx.storage.sql.exec(`

      CREATE TABLE IF NOT EXISTS _sql_schema_migrations (

        id INTEGER PRIMARY KEY,

        applied_at TEXT NOT NULL DEFAULT (datetime('now'))

      );

    `);


    // Determine the current schema version

    const version =

      this.ctx.storage.sql

        .exec<{ version: number }>(

          "SELECT COALESCE(MAX(id), 0) as version FROM _sql_schema_migrations",

        )

        .one().version;


    if (version < 1) {

      this.ctx.storage.sql.exec(`

        CREATE TABLE IF NOT EXISTS messages (

          id INTEGER PRIMARY KEY AUTOINCREMENT,

          user_id TEXT NOT NULL,

          content TEXT NOT NULL,

          created_at INTEGER NOT NULL

        );

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

        INSERT INTO _sql_schema_migrations (id) VALUES (1);

      `);

    }


    if (version < 2) {

      // Future migration: add a new column

      this.ctx.storage.sql.exec(`

        ALTER TABLE messages ADD COLUMN edited_at INTEGER;

        INSERT INTO _sql_schema_migrations (id) VALUES (2);

      `);

    }

  }

}


```

### Understand the difference between in-memory state and persistent storage

Durable Objects provide multiple state management layers, each with different characteristics:

| Type                         | Speed    | Persistence                  | Use Case                    |
| ---------------------------- | -------- | ---------------------------- | --------------------------- |
| In-memory (class properties) | Fastest  | Lost on eviction or crash    | Caching, active connections |
| SQLite storage               | Fast     | Durable across restarts      | Primary data storage        |
| External (R2, D1)            | Variable | Durable, cross-DO accessible | Large files, shared data    |

In-memory state is **not preserved** if the Durable Object is evicted from memory due to inactivity, or if it crashes from an uncaught exception. Always persist important state to SQLite storage.

* [  JavaScript ](#tab-panel-5991)
* [  TypeScript ](#tab-panel-5992)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class ChatRoom extends DurableObject {

  // In-memory cache - fast but NOT preserved across evictions or crashes

  messageCache = null;


  async getRecentMessages() {

    // Return from cache if available (only valid while DO is in memory)

    if (this.messageCache !== null) {

      return this.messageCache;

    }


    // Otherwise, load from durable storage

    const cursor = this.ctx.storage.sql.exec(

      "SELECT * FROM messages ORDER BY created_at DESC LIMIT 100",

    );

    this.messageCache = cursor.toArray();

    return this.messageCache;

  }


  async addMessage(userId, content) {

    // ✅ Always persist to durable storage first

    this.ctx.storage.sql.exec(

      "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)",

      userId,

      content,

      Date.now(),

    );


    // Then update the cache (if it exists)

    // If the DO crashes here, the message is still saved in SQLite

    this.messageCache = null; // Invalidate cache

  }

}


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  CHAT_ROOM: DurableObjectNamespace<ChatRoom>;

}


type Message = {

id: number;

user_id: string;

content: string;

created_at: number;

};


export class ChatRoom extends DurableObject<Env> {

  // In-memory cache - fast but NOT preserved across evictions or crashes

  private messageCache: Message[] | null = null;


    async getRecentMessages(): Promise<Message[]> {

      // Return from cache if available (only valid while DO is in memory)

      if (this.messageCache !== null) {

        return this.messageCache;

      }


      // Otherwise, load from durable storage

      const cursor = this.ctx.storage.sql.exec<Message>(

        "SELECT * FROM messages ORDER BY created_at DESC LIMIT 100"

      );

      this.messageCache = cursor.toArray();

      return this.messageCache;

    }


    async addMessage(userId: string, content: string) {

      // ✅ Always persist to durable storage first

      this.ctx.storage.sql.exec(

        "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)",

        userId,

        content,

        Date.now()

      );


      // Then update the cache (if it exists)

      // If the DO crashes here, the message is still saved in SQLite

      this.messageCache = null; // Invalidate cache

    }

}


```

Warning

If an uncaught exception occurs in your Durable Object, the runtime may terminate the instance. Any in-memory state will be lost, but SQLite storage remains intact. Always persist critical state to storage before performing operations that might fail.

### Create indexes for frequently-queried columns

Just like any database, indexes dramatically improve read performance for frequently-filtered columns. The cost is slightly more storage and marginally slower writes.

* [  JavaScript ](#tab-panel-5989)
* [  TypeScript ](#tab-panel-5990)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class ChatRoom extends DurableObject {

  constructor(ctx, env) {

    super(ctx, env);


    ctx.blockConcurrencyWhile(async () => {

      this.ctx.storage.sql.exec(`

        CREATE TABLE IF NOT EXISTS messages (

          id INTEGER PRIMARY KEY AUTOINCREMENT,

          user_id TEXT NOT NULL,

          content TEXT NOT NULL,

          created_at INTEGER NOT NULL

        );


        -- Index for queries filtering by user

        CREATE INDEX IF NOT EXISTS idx_messages_user_id ON messages(user_id);


        -- Index for time-based queries (recent messages)

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


        -- Composite index for user + time queries

        CREATE INDEX IF NOT EXISTS idx_messages_user_time ON messages(user_id, created_at);

      `);

    });

  }


  // This query benefits from idx_messages_user_time

  async getUserMessages(userId, since) {

    return this.ctx.storage.sql

      .exec(

        "SELECT * FROM messages WHERE user_id = ? AND created_at > ? ORDER BY created_at",

        userId,

        since,

      )

      .toArray();

  }

}


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  CHAT_ROOM: DurableObjectNamespace<ChatRoom>;

}


export class ChatRoom extends DurableObject<Env> {

  constructor(ctx: DurableObjectState, env: Env) {

    super(ctx, env);


    ctx.blockConcurrencyWhile(async () => {

      this.ctx.storage.sql.exec(`

        CREATE TABLE IF NOT EXISTS messages (

          id INTEGER PRIMARY KEY AUTOINCREMENT,

          user_id TEXT NOT NULL,

          content TEXT NOT NULL,

          created_at INTEGER NOT NULL

        );


        -- Index for queries filtering by user

        CREATE INDEX IF NOT EXISTS idx_messages_user_id ON messages(user_id);


        -- Index for time-based queries (recent messages)

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


        -- Composite index for user + time queries

        CREATE INDEX IF NOT EXISTS idx_messages_user_time ON messages(user_id, created_at);

      `);

    });

  }


  // This query benefits from idx_messages_user_time

  async getUserMessages(userId: string, since: number) {

    return this.ctx.storage.sql

      .exec(

        "SELECT * FROM messages WHERE user_id = ? AND created_at > ? ORDER BY created_at",

        userId,

        since

      )

      .toArray();

  }

}


```

### Understand how input and output gates work

While Durable Objects are single-threaded, JavaScript's `async`/`await` can allow multiple requests to interleave execution while a request waits for the result of an asynchronous operation. Cloudflare's runtime uses **input gates** and **output gates** to prevent data races and ensure correctness by default.

**Input gates** block new events (incoming requests, fetch responses) while synchronous JavaScript execution is in progress. Awaiting async operations like `fetch()` or KV storage methods opens the input gate, allowing other requests to interleave. However, storage operations provide special protection:

* [  JavaScript ](#tab-panel-5979)
* [  TypeScript ](#tab-panel-5980)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class Counter extends DurableObject {

  // This code is safe due to input gates

  async increment() {

    // While these storage operations execute, no other requests

    // can interleave - input gate blocks new events

    const value = (await this.ctx.storage.get("count")) ?? 0;

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

    return value + 1;

  }

}


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  COUNTER: DurableObjectNamespace<Counter>;

}


export class Counter extends DurableObject<Env> {

  // This code is safe due to input gates

  async increment(): Promise<number> {

    // While these storage operations execute, no other requests

    // can interleave - input gate blocks new events

    const value = (await this.ctx.storage.get<number>("count")) ?? 0;

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

    return value + 1;

  }

}


```

**Output gates** hold outgoing network messages (responses, fetch requests) until pending storage writes complete. This ensures clients never see confirmation of data that has not been persisted:

* [  JavaScript ](#tab-panel-5981)
* [  TypeScript ](#tab-panel-5982)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class ChatRoom extends DurableObject {

  async sendMessage(userId, content) {

    // Write to storage - don't need to await for correctness

    this.ctx.storage.sql.exec(

      "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)",

      userId,

      content,

      Date.now(),

    );


    // This response is held by the output gate until the write completes.

    // The client only receives "Message sent" after data is safely persisted.

    return "Message sent";

  }

}


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  CHAT_ROOM: DurableObjectNamespace<ChatRoom>;

}


export class ChatRoom extends DurableObject<Env> {

  async sendMessage(userId: string, content: string): Promise<string> {

    // Write to storage - don't need to await for correctness

    this.ctx.storage.sql.exec(

      "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)",

      userId,

      content,

      Date.now()

    );


      // This response is held by the output gate until the write completes.

      // The client only receives "Message sent" after data is safely persisted.

      return "Message sent";

    }

}


```

**Write coalescing:** Multiple storage writes without intervening `await` calls are automatically batched into a single atomic implicit transaction:

* [  JavaScript ](#tab-panel-5993)
* [  TypeScript ](#tab-panel-5994)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class Account extends DurableObject {

  async transfer(fromId, toId, amount) {

    // ✅ Good: These writes are coalesced into one atomic transaction

    this.ctx.storage.sql.exec(

      "UPDATE accounts SET balance = balance - ? WHERE id = ?",

      amount,

      fromId,

    );

    this.ctx.storage.sql.exec(

      "UPDATE accounts SET balance = balance + ? WHERE id = ?",

      amount,

      toId,

    );

    this.ctx.storage.sql.exec(

      "INSERT INTO transfers (from_id, to_id, amount, created_at) VALUES (?, ?, ?, ?)",

      fromId,

      toId,

      amount,

      Date.now(),

    );

    // All three writes commit together atomically

  }


  // 🔴 Bad: await on KV operations breaks coalescing

  async transferBrokenKV(fromId, toId, amount) {

    const fromBalance = (await this.ctx.storage.get(`balance:${fromId}`)) ?? 0;

    await this.ctx.storage.put(`balance:${fromId}`, fromBalance - amount);

    // If the next write fails, the debit already committed!

    const toBalance = (await this.ctx.storage.get(`balance:${toId}`)) ?? 0;

    await this.ctx.storage.put(`balance:${toId}`, toBalance + amount);

  }

}


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  ACCOUNT: DurableObjectNamespace<Account>;

}


export class Account extends DurableObject<Env> {

  async transfer(fromId: string, toId: string, amount: number) {

    // ✅ Good: These writes are coalesced into one atomic transaction

    this.ctx.storage.sql.exec(

      "UPDATE accounts SET balance = balance - ? WHERE id = ?",

      amount,

      fromId

    );

    this.ctx.storage.sql.exec(

      "UPDATE accounts SET balance = balance + ? WHERE id = ?",

      amount,

      toId

    );

    this.ctx.storage.sql.exec(

      "INSERT INTO transfers (from_id, to_id, amount, created_at) VALUES (?, ?, ?, ?)",

      fromId,

      toId,

      amount,

      Date.now()

    );

    // All three writes commit together atomically

  }


  // 🔴 Bad: await on KV operations breaks coalescing

  async transferBrokenKV(fromId: string, toId: string, amount: number) {

    const fromBalance = (await this.ctx.storage.get<number>(`balance:${fromId}`)) ?? 0;

    await this.ctx.storage.put(`balance:${fromId}`, fromBalance - amount);

    // If the next write fails, the debit already committed!

    const toBalance = (await this.ctx.storage.get<number>(`balance:${toId}`)) ?? 0;

    await this.ctx.storage.put(`balance:${toId}`, toBalance + amount);

  }

}


```

For more details, see [Durable Objects: Easy, Fast, Correct — Choose three ↗](https://blog.cloudflare.com/durable-objects-easy-fast-correct-choose-three/) and the [glossary](https://developers.cloudflare.com/durable-objects/reference/glossary/).

### Avoid race conditions with non-storage I/O

Input gates only protect during storage operations. Non-storage I/O like `fetch()` or writing to R2 allows other requests to interleave, which can cause race conditions:

* [  JavaScript ](#tab-panel-5983)
* [  TypeScript ](#tab-panel-5984)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class Processor extends DurableObject {

  // ⚠️ Potential race condition: fetch() allows interleaving

  async processItem(id) {

    const item = await this.ctx.storage.get(`item:${id}`);


    if (item?.status === "pending") {

      // During this fetch, other requests CAN execute and modify storage

      const result = await fetch("https://api.example.com/process");


      // Another request may have already processed this item!

      await this.ctx.storage.put(`item:${id}`, { status: "completed" });

    }

  }

}


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  PROCESSOR: DurableObjectNamespace<Processor>;

}


export class Processor extends DurableObject<Env> {

  // ⚠️ Potential race condition: fetch() allows interleaving

  async processItem(id: string) {

    const item = await this.ctx.storage.get<{ status: string }>(`item:${id}`);


      if (item?.status === "pending") {

        // During this fetch, other requests CAN execute and modify storage

        const result = await fetch("https://api.example.com/process");


        // Another request may have already processed this item!

        await this.ctx.storage.put(`item:${id}`, { status: "completed" });

      }

    }

}


```

To handle this, use optimistic locking (check-and-set) patterns: read a version number before the external call, then verify it has not changed before writing.

Note

With the legacy KV storage backend, use the [transaction()](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/#transaction) method for atomic read-modify-write operations across async boundaries.

### Use `blockConcurrencyWhile()` sparingly

The [blockConcurrencyWhile()](https://developers.cloudflare.com/durable-objects/api/state/#blockconcurrencywhile) method guarantees that no other events are processed until the provided callback completes, even if the callback performs asynchronous I/O. This is useful for operations that must be atomic, such as state initialization from storage in the constructor:

* [  JavaScript ](#tab-panel-6001)
* [  TypeScript ](#tab-panel-6002)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class ChatRoom extends DurableObject {

  constructor(ctx, env) {

    super(ctx, env);


    // ✅ Good: Use blockConcurrencyWhile for one-time initialization

    ctx.blockConcurrencyWhile(async () => {

      this.ctx.storage.sql.exec(`

        CREATE TABLE IF NOT EXISTS messages (

          id INTEGER PRIMARY KEY,

          content TEXT

        )

      `);

    });

  }


  // 🔴 Bad: Don't use blockConcurrencyWhile on every request

  async sendMessageSlow(content) {

    await this.ctx.blockConcurrencyWhile(async () => {

      this.ctx.storage.sql.exec(

        "INSERT INTO messages (content) VALUES (?)",

        content,

      );

    });

    // If this takes ~5ms, you're limited to ~200 requests/second

  }


  // ✅ Good: Let output gates handle consistency

  async sendMessageFast(content) {

    this.ctx.storage.sql.exec(

      "INSERT INTO messages (content) VALUES (?)",

      content,

    );

    // Output gate ensures write completes before response is sent

    // Other requests can be processed concurrently

  }

}


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  CHAT_ROOM: DurableObjectNamespace<ChatRoom>;

}


export class ChatRoom extends DurableObject<Env> {

  constructor(ctx: DurableObjectState, env: Env) {

    super(ctx, env);


    // ✅ Good: Use blockConcurrencyWhile for one-time initialization

    ctx.blockConcurrencyWhile(async () => {

      this.ctx.storage.sql.exec(`

        CREATE TABLE IF NOT EXISTS messages (

          id INTEGER PRIMARY KEY,

          content TEXT

        )

      `);

    });

  }


  // 🔴 Bad: Don't use blockConcurrencyWhile on every request

  async sendMessageSlow(content: string) {

    await this.ctx.blockConcurrencyWhile(async () => {

      this.ctx.storage.sql.exec(

        "INSERT INTO messages (content) VALUES (?)",

        content

      );

    });

    // If this takes ~5ms, you're limited to ~200 requests/second

  }


  // ✅ Good: Let output gates handle consistency

  async sendMessageFast(content: string) {

    this.ctx.storage.sql.exec(

      "INSERT INTO messages (content) VALUES (?)",

      content

    );

    // Output gate ensures write completes before response is sent

    // Other requests can be processed concurrently

  }

}


```

Because `blockConcurrencyWhile()` blocks _all_ concurrency unconditionally, it significantly reduces throughput. If each call takes \~5ms, that individual Durable Object is limited to approximately 200 requests/second. Reserve it for initialization and migrations, not regular request handling. For normal operations, rely on input/output gates and write coalescing instead.

For atomic read-modify-write operations during request handling, prefer [transaction()](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/#transaction) over `blockConcurrencyWhile()`. Transactions provide atomicity for storage operations without blocking unrelated concurrent requests.

Warning

Using `blockConcurrencyWhile()` across I/O operations (such as `fetch()`, KV, R2, or other external API calls) is an anti-pattern. This is equivalent to holding a lock across I/O in other languages or concurrency frameworks — it blocks all other requests while waiting for slow external operations, severely degrading throughput. Keep `blockConcurrencyWhile()` callbacks fast and limited to local storage operations.

## Communication and API design

### Use RPC methods instead of the `fetch()` handler

Projects with a [compatibility date](https://developers.cloudflare.com/workers/configuration/compatibility-flags/) of `2024-04-03` or later should use RPC methods. RPC is more ergonomic, provides better type safety, and eliminates manual request/response parsing.

Define public methods on your Durable Object class, and call them directly from stubs with full TypeScript support:

* [  JavaScript ](#tab-panel-6019)
* [  TypeScript ](#tab-panel-6020)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class ChatRoom extends DurableObject {

  // Public methods are automatically exposed as RPC endpoints

  async sendMessage(userId, content) {

    const createdAt = Date.now();

    const result = this.ctx.storage.sql.exec(

      "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?) RETURNING id",

      userId,

      content,

      createdAt,

    );

    const { id } = result.one();

    return { id, userId, content, createdAt };

  }


  async getMessages(limit = 50) {

    const cursor = this.ctx.storage.sql.exec(

      "SELECT * FROM messages ORDER BY created_at DESC LIMIT ?",

      limit,

    );


    return cursor.toArray().map((row) => ({

      id: row.id,

      userId: row.user_id,

      content: row.content,

      createdAt: row.created_at,

    }));

  }

}


export default {

  async fetch(request, env) {

    const url = new URL(request.url);

    const roomId = url.searchParams.get("room") ?? "lobby";


    const id = env.CHAT_ROOM.idFromName(roomId);

    // stub is typed as DurableObjectStub<ChatRoom>

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


    if (request.method === "POST") {

      const { userId, content } = await request.json();

      // Direct method call with full type checking

      const message = await stub.sendMessage(userId, content);

      return Response.json(message);

    }


    // TypeScript knows getMessages() returns Promise<Message[]>

    const messages = await stub.getMessages(100);

    return Response.json(messages);

  },

};


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  // Type parameter provides typed method calls on the stub

  CHAT_ROOM: DurableObjectNamespace<ChatRoom>;

}


type Message = {

id: number;

userId: string;

content: string;

createdAt: number;

};


export class ChatRoom extends DurableObject<Env> {

  // Public methods are automatically exposed as RPC endpoints

  async sendMessage(userId: string, content: string): Promise<Message> {

    const createdAt = Date.now();

    const result = this.ctx.storage.sql.exec<{ id: number }>(

      "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?) RETURNING id",

      userId,

      content,

      createdAt

    );

    const { id } = result.one();

    return { id, userId, content, createdAt };

  }


    async getMessages(limit: number = 50): Promise<Message[]> {

      const cursor = this.ctx.storage.sql.exec<{

        id: number;

        user_id: string;

        content: string;

        created_at: number;

      }>("SELECT * FROM messages ORDER BY created_at DESC LIMIT ?", limit);


      return cursor.toArray().map((row) => ({

        id: row.id,

        userId: row.user_id,

        content: row.content,

        createdAt: row.created_at,

      }));

    }

}


export default {

  async fetch(request: Request, env: Env): Promise<Response> {

    const url = new URL(request.url);

    const roomId = url.searchParams.get("room") ?? "lobby";


      const id = env.CHAT_ROOM.idFromName(roomId);

      // stub is typed as DurableObjectStub<ChatRoom>

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


      if (request.method === "POST") {

        const { userId, content } = await request.json<{

          userId: string;

          content: string;

        }>();

        // Direct method call with full type checking

        const message = await stub.sendMessage(userId, content);

        return Response.json(message);

      }


      // TypeScript knows getMessages() returns Promise<Message[]>

      const messages = await stub.getMessages(100);

      return Response.json(messages);

    },

};


```

Refer to [Invoke methods](https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/) for more details on RPC and the legacy `fetch()` handler.

### Initialize Durable Objects explicitly with an `init()` method

Durable Objects do not know their own name or ID from within. If your Durable Object needs to know its identity (for example, to store a reference to itself or to communicate with related objects), you must explicitly initialize it.

* [  JavaScript ](#tab-panel-6011)
* [  TypeScript ](#tab-panel-6012)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class ChatRoom extends DurableObject {

  roomId = null;


  // Call this after creating the Durable Object for the first time

  async init(roomId, createdBy) {

    // Check if already initialized

    const existing = await this.ctx.storage.get("roomId");

    if (existing) {

      return; // Already initialized

    }


    // Store the identity

    await this.ctx.storage.put("roomId", roomId);

    await this.ctx.storage.put("createdBy", createdBy);

    await this.ctx.storage.put("createdAt", Date.now());


    // Cache in memory for this session

    this.roomId = roomId;

  }


  async getRoomId() {

    if (this.roomId) {

      return this.roomId;

    }


    const stored = await this.ctx.storage.get("roomId");

    if (!stored) {

      throw new Error("ChatRoom not initialized. Call init() first.");

    }


    this.roomId = stored;

    return stored;

  }

}


export default {

  async fetch(request, env) {

    const url = new URL(request.url);

    const roomId = url.searchParams.get("room") ?? "lobby";


    const id = env.CHAT_ROOM.idFromName(roomId);

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


    // Initialize on first access

    await stub.init(roomId, "system");


    return new Response(`Room ${await stub.getRoomId()} ready`);

  },

};


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  CHAT_ROOM: DurableObjectNamespace<ChatRoom>;

}


export class ChatRoom extends DurableObject<Env> {

  private roomId: string | null = null;


  // Call this after creating the Durable Object for the first time

  async init(roomId: string, createdBy: string) {

    // Check if already initialized

    const existing = await this.ctx.storage.get("roomId");

    if (existing) {

      return; // Already initialized

    }


    // Store the identity

    await this.ctx.storage.put("roomId", roomId);

    await this.ctx.storage.put("createdBy", createdBy);

    await this.ctx.storage.put("createdAt", Date.now());


    // Cache in memory for this session

    this.roomId = roomId;

  }


  async getRoomId(): Promise<string> {

    if (this.roomId) {

      return this.roomId;

    }


    const stored = await this.ctx.storage.get<string>("roomId");

    if (!stored) {

      throw new Error("ChatRoom not initialized. Call init() first.");

    }


    this.roomId = stored;

    return stored;

  }

}


export default {

  async fetch(request: Request, env: Env): Promise<Response> {

    const url = new URL(request.url);

    const roomId = url.searchParams.get("room") ?? "lobby";


    const id = env.CHAT_ROOM.idFromName(roomId);

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


    // Initialize on first access

    await stub.init(roomId, "system");


    return new Response(`Room ${await stub.getRoomId()} ready`);

  },

};


```

### Always `await` RPC calls

When calling methods on a Durable Object stub, always use `await`. Unawaited calls create dangling promises, causing errors to be swallowed and return values to be lost.

* [  JavaScript ](#tab-panel-5997)
* [  TypeScript ](#tab-panel-5998)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class ChatRoom extends DurableObject {

  async sendMessage(userId, content) {

    const result = this.ctx.storage.sql.exec(

      "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?) RETURNING id",

      userId,

      content,

      Date.now(),

    );

    return result.one().id;

  }

}


export default {

  async fetch(request, env) {

    const id = env.CHAT_ROOM.idFromName("lobby");

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


    // 🔴 Bad: Not awaiting the call

    // The message ID is lost, and any errors are swallowed

    stub.sendMessage("user-123", "Hello");


    // ✅ Good: Properly awaited

    const messageId = await stub.sendMessage("user-123", "Hello");


    return Response.json({ messageId });

  },

};


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  CHAT_ROOM: DurableObjectNamespace<ChatRoom>;

}


export class ChatRoom extends DurableObject<Env> {

  async sendMessage(userId: string, content: string): Promise<number> {

    const result = this.ctx.storage.sql.exec<{ id: number }>(

      "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?) RETURNING id",

      userId,

      content,

      Date.now()

    );

    return result.one().id;

  }

}


export default {

  async fetch(request: Request, env: Env): Promise<Response> {

    const id = env.CHAT_ROOM.idFromName("lobby");

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


      // 🔴 Bad: Not awaiting the call

      // The message ID is lost, and any errors are swallowed

      stub.sendMessage("user-123", "Hello");


      // ✅ Good: Properly awaited

      const messageId = await stub.sendMessage("user-123", "Hello");


      return Response.json({ messageId });

    },

};


```

## Error handling

### Handle errors and use exception boundaries

Uncaught exceptions in a Durable Object can leave it in an unknown state and may cause the runtime to terminate the instance. Wrap risky operations in `try...catch` blocks, and handle errors appropriately.

* [  JavaScript ](#tab-panel-6005)
* [  TypeScript ](#tab-panel-6006)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class ChatRoom extends DurableObject {

  async processMessage(userId, content) {

    // ✅ Good: Wrap risky operations in try...catch

    try {

      // Validate input before processing

      if (!content || content.length > 10000) {

        throw new Error("Invalid message content");

      }


      this.ctx.storage.sql.exec(

        "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)",

        userId,

        content,

        Date.now(),

      );


      // External call that might fail

      await this.notifySubscribers(content);

    } catch (error) {

      // Log the error for debugging

      console.error("Failed to process message:", error);


      // Re-throw if it's a validation error (don't retry)

      if (error instanceof Error && error.message.includes("Invalid")) {

        throw error;

      }


      // For transient errors, you might want to handle differently

      throw error;

    }

  }


  async notifySubscribers(content) {

    // External notification logic

  }

}


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  CHAT_ROOM: DurableObjectNamespace<ChatRoom>;

}


export class ChatRoom extends DurableObject<Env> {

  async processMessage(userId: string, content: string) {

    // ✅ Good: Wrap risky operations in try...catch

    try {

      // Validate input before processing

      if (!content || content.length > 10000) {

        throw new Error("Invalid message content");

      }


      this.ctx.storage.sql.exec(

        "INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)",

        userId,

        content,

        Date.now()

      );


      // External call that might fail

      await this.notifySubscribers(content);

    } catch (error) {

      // Log the error for debugging

      console.error("Failed to process message:", error);


      // Re-throw if it's a validation error (don't retry)

      if (error instanceof Error && error.message.includes("Invalid")) {

        throw error;

      }


      // For transient errors, you might want to handle differently

      throw error;

    }

  }


  private async notifySubscribers(content: string) {

    // External notification logic

  }

}


```

When calling Durable Objects from a Worker, errors may include `.retryable` and `.overloaded` properties indicating whether the operation can be retried. For transient failures, implement exponential backoff to avoid overwhelming the system.

Refer to [Error handling](https://developers.cloudflare.com/durable-objects/best-practices/error-handling/) for details on error properties, retry strategies, and exponential backoff patterns.

## WebSockets and real-time

### Use the Hibernatable WebSockets API for cost efficiency

The Hibernatable WebSockets API allows Durable Objects to sleep while maintaining WebSocket connections. This significantly reduces costs for applications with many idle connections.

* [  JavaScript ](#tab-panel-6017)
* [  TypeScript ](#tab-panel-6018)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class ChatRoom extends DurableObject {

  async fetch(request) {

    const url = new URL(request.url);


    if (url.pathname === "/websocket") {

      // Check for WebSocket upgrade

      if (request.headers.get("Upgrade") !== "websocket") {

        return new Response("Expected WebSocket", { status: 400 });

      }


      const pair = new WebSocketPair();

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


      // Accept the WebSocket with Hibernation API

      this.ctx.acceptWebSocket(server);


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

    }


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

  }


  // Called when a message is received (even after hibernation)

  async webSocketMessage(ws, message) {

    const data = typeof message === "string" ? message : "binary data";


    // Broadcast to all connected clients

    for (const client of this.ctx.getWebSockets()) {

      if (client !== ws && client.readyState === WebSocket.OPEN) {

        client.send(data);

      }

    }

  }


  // Called when a WebSocket is closed

  async webSocketClose(ws, code, reason, wasClean) {

    // With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime

    // auto-replies to Close frames. Calling close() is safe but no longer required.

    ws.close(code, reason);

    console.log(`WebSocket closed: ${code} ${reason}`);

  }


  // Called when a WebSocket error occurs

  async webSocketError(ws, error) {

    console.error("WebSocket error:", error);

  }

}


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  CHAT_ROOM: DurableObjectNamespace<ChatRoom>;

}


export class ChatRoom extends DurableObject<Env> {

  async fetch(request: Request): Promise<Response> {

    const url = new URL(request.url);


      if (url.pathname === "/websocket") {

        // Check for WebSocket upgrade

        if (request.headers.get("Upgrade") !== "websocket") {

          return new Response("Expected WebSocket", { status: 400 });

        }


        const pair = new WebSocketPair();

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


        // Accept the WebSocket with Hibernation API

        this.ctx.acceptWebSocket(server);


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

      }


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

    }


    // Called when a message is received (even after hibernation)

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

      const data = typeof message === "string" ? message : "binary data";


      // Broadcast to all connected clients

      for (const client of this.ctx.getWebSockets()) {

        if (client !== ws && client.readyState === WebSocket.OPEN) {

          client.send(data);

        }

      }

    }


    // Called when a WebSocket is closed

    async webSocketClose(

      ws: WebSocket,

      code: number,

      reason: string,

      wasClean: boolean

    ) {

      // With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime

      // auto-replies to Close frames. Calling close() is safe but no longer required.

      ws.close(code, reason);

      console.log(`WebSocket closed: ${code} ${reason}`);

    }


    // Called when a WebSocket error occurs

    async webSocketError(ws: WebSocket, error: unknown) {

      console.error("WebSocket error:", error);

    }

}


```

With the Hibernation API, your Durable Object can go to sleep when there is no active JavaScript execution, but WebSocket connections remain open. When a message arrives, the Durable Object wakes up automatically.

Best practices:

* The [WebSocket Hibernation API](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api) exposes `webSocketError`, `webSocketMessage`, and `webSocketClose` handlers for their respective WebSocket events.
* With the [web\_socket\_auto\_reply\_to\_close](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#websocket-auto-reply-to-close) compatibility flag (enabled by default on compatibility dates on or after `2026-04-07`), the runtime automatically completes the close handshake. Calling `ws.close()` in `webSocketClose` is still safe but no longer required. On older compatibility dates, you **must** call `ws.close()` to avoid `1006` abnormal closure errors.

Refer to [WebSockets](https://developers.cloudflare.com/durable-objects/best-practices/websockets/) for more details.

### Use `serializeAttachment()` to persist per-connection state

WebSocket attachments let you store metadata for each connection that survives hibernation. Use this for user IDs, session tokens, or other per-connection data.

* [  JavaScript ](#tab-panel-6021)
* [  TypeScript ](#tab-panel-6022)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class ChatRoom extends DurableObject {

  async fetch(request) {

    const url = new URL(request.url);


    if (url.pathname === "/websocket") {

      if (request.headers.get("Upgrade") !== "websocket") {

        return new Response("Expected WebSocket", { status: 400 });

      }


      const userId = url.searchParams.get("userId") ?? "anonymous";

      const username = url.searchParams.get("username") ?? "Anonymous";


      const pair = new WebSocketPair();

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


      this.ctx.acceptWebSocket(server);


      // Store per-connection state that survives hibernation

      const state = {

        userId,

        username,

        joinedAt: Date.now(),

      };

      server.serializeAttachment(state);


      // Broadcast join message

      this.broadcast(`${username} joined the chat`);


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

    }


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

  }


  async webSocketMessage(ws, message) {

    // Retrieve the connection state (works even after hibernation)

    const state = ws.deserializeAttachment();


    const chatMessage = JSON.stringify({

      userId: state.userId,

      username: state.username,

      content: message,

      timestamp: Date.now(),

    });


    this.broadcast(chatMessage);

  }


  async webSocketClose(ws, code, reason) {

    // With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime

    // auto-replies to Close frames. Calling close() is safe but no longer required.

    ws.close(code, reason);

    const state = ws.deserializeAttachment();

    this.broadcast(`${state.username} left the chat`);

  }


  broadcast(message) {

    for (const client of this.ctx.getWebSockets()) {

      if (client.readyState === WebSocket.OPEN) {

        client.send(message);

      }

    }

  }

}


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  CHAT_ROOM: DurableObjectNamespace<ChatRoom>;

}


type ConnectionState = {

  userId: string;

  username: string;

  joinedAt: number;

};


export class ChatRoom extends DurableObject<Env> {

  async fetch(request: Request): Promise<Response> {

    const url = new URL(request.url);


    if (url.pathname === "/websocket") {

      if (request.headers.get("Upgrade") !== "websocket") {

        return new Response("Expected WebSocket", { status: 400 });

      }


      const userId = url.searchParams.get("userId") ?? "anonymous";

      const username = url.searchParams.get("username") ?? "Anonymous";


      const pair = new WebSocketPair();

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


      this.ctx.acceptWebSocket(server);


      // Store per-connection state that survives hibernation

      const state: ConnectionState = {

        userId,

        username,

        joinedAt: Date.now(),

      };

      server.serializeAttachment(state);


      // Broadcast join message

      this.broadcast(`${username} joined the chat`);


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

    }


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

  }


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

    // Retrieve the connection state (works even after hibernation)

    const state = ws.deserializeAttachment() as ConnectionState;


    const chatMessage = JSON.stringify({

      userId: state.userId,

      username: state.username,

      content: message,

      timestamp: Date.now(),

    });


    this.broadcast(chatMessage);

  }


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

    // With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime

    // auto-replies to Close frames. Calling close() is safe but no longer required.

    ws.close(code, reason);

    const state = ws.deserializeAttachment() as ConnectionState;

    this.broadcast(`${state.username} left the chat`);

  }


  private broadcast(message: string) {

    for (const client of this.ctx.getWebSockets()) {

      if (client.readyState === WebSocket.OPEN) {

        client.send(message);

      }

    }

  }

}


```

## Scheduling and lifecycle

### Use alarms for per-entity scheduled tasks

Each Durable Object can schedule its own future work using the [Alarms API](https://developers.cloudflare.com/durable-objects/api/alarms/), allowing a Durable Object to execute background tasks on any interval without an incoming request, RPC call, or WebSocket message.

Key points about alarms:

* **`setAlarm(timestamp)`** schedules the `alarm()` handler to run at any time in the future (millisecond precision)
* **Alarms do not repeat automatically** — you must call `setAlarm()` again to schedule the next execution
* **Only schedule alarms when there is work to do** — avoid waking up every Durable Object on short intervals (seconds), as each alarm invocation incurs costs

* [  JavaScript ](#tab-panel-6015)
* [  TypeScript ](#tab-panel-6016)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class GameMatch extends DurableObject {

  async startGame(durationMs = 60000) {

    await this.ctx.storage.put("gameStarted", Date.now());

    await this.ctx.storage.put("gameActive", true);


    // Schedule the game to end after the duration

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

  }


  // Called when the alarm fires

  async alarm(alarmInfo) {

    const isActive = await this.ctx.storage.get("gameActive");


    if (!isActive) {

      return; // Game was already ended

    }


    // End the game

    await this.ctx.storage.put("gameActive", false);

    await this.ctx.storage.put("gameEnded", Date.now());


    // Calculate final scores, notify players, etc.

    try {

      await this.calculateFinalScores();

    } catch (err) {

      // If we're almost out of retries but still have work to do, schedule a new alarm

      // rather than letting our retries run out to ensure we keep getting invoked.

      if (alarmInfo && alarmInfo.retryCount >= 5) {

        await this.ctx.storage.setAlarm(Date.now() + 30 * 1000);

        return;

      }

      throw err;

    }


    // Schedule the next alarm only if there's more work to do

    // In this case, schedule cleanup in 24 hours

    await this.ctx.storage.setAlarm(Date.now() + 24 * 60 * 60 * 1000);

  }


  async calculateFinalScores() {

    // Game ending logic

  }

}


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  GAME_MATCH: DurableObjectNamespace<GameMatch>;

}


export class GameMatch extends DurableObject<Env> {

  async startGame(durationMs: number = 60000) {

    await this.ctx.storage.put("gameStarted", Date.now());

    await this.ctx.storage.put("gameActive", true);


      // Schedule the game to end after the duration

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

    }


    // Called when the alarm fires

    async alarm(alarmInfo?: AlarmInvocationInfo) {

      const isActive = await this.ctx.storage.get<boolean>("gameActive");


      if (!isActive) {

        return; // Game was already ended

      }


      // End the game

      await this.ctx.storage.put("gameActive", false);

      await this.ctx.storage.put("gameEnded", Date.now());


      // Calculate final scores, notify players, etc.

      try {

        await this.calculateFinalScores();

      } catch (err) {

        // If we're almost out of retries but still have work to do, schedule a new alarm

        // rather than letting our retries run out to ensure we keep getting invoked.

        if (alarmInfo && alarmInfo.retryCount >= 5) {

          await this.ctx.storage.setAlarm(Date.now() + 30 * 1000);

          return;

        }

        throw err;

      }


      // Schedule the next alarm only if there's more work to do

      // In this case, schedule cleanup in 24 hours

      await this.ctx.storage.setAlarm(Date.now() + 24 * 60 * 60 * 1000);

    }


    private async calculateFinalScores() {

      // Game ending logic

    }

}


```

### Make alarm handlers idempotent

In rare cases, alarms may fire more than once. Your `alarm()` handler should be safe to run multiple times without causing issues.

* [  JavaScript ](#tab-panel-6007)
* [  TypeScript ](#tab-panel-6008)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class Subscription extends DurableObject {

  async alarm() {

    // ✅ Good: Check state before performing the action

    const lastRenewal = await this.ctx.storage.get("lastRenewal");

    const renewalPeriod = 30 * 24 * 60 * 60 * 1000; // 30 days


    // If we already renewed recently, don't do it again

    if (lastRenewal && Date.now() - lastRenewal < renewalPeriod - 60000) {

      console.log("Already renewed recently, skipping");

      return;

    }


    // Perform the renewal

    const success = await this.processRenewal();


    if (success) {

      // Record the renewal time

      await this.ctx.storage.put("lastRenewal", Date.now());


      // Schedule the next renewal

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

    } else {

      // Retry in 1 hour

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

    }

  }


  async processRenewal() {

    // Payment processing logic

    return true;

  }

}


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  SUBSCRIPTION: DurableObjectNamespace<Subscription>;

}


export class Subscription extends DurableObject<Env> {

  async alarm() {

    // ✅ Good: Check state before performing the action

    const lastRenewal = await this.ctx.storage.get<number>("lastRenewal");

    const renewalPeriod = 30 * 24 * 60 * 60 * 1000; // 30 days


    // If we already renewed recently, don't do it again

    if (lastRenewal && Date.now() - lastRenewal < renewalPeriod - 60000) {

      console.log("Already renewed recently, skipping");

      return;

    }


    // Perform the renewal

    const success = await this.processRenewal();


    if (success) {

      // Record the renewal time

      await this.ctx.storage.put("lastRenewal", Date.now());


      // Schedule the next renewal

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

    } else {

      // Retry in 1 hour

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

    }

  }


  private async processRenewal(): Promise<boolean> {

    // Payment processing logic

    return true;

  }

}


```

### Clean up storage with `deleteAll()`

To fully clear a Durable Object's storage, call `deleteAll()`. Simply deleting individual keys or dropping tables is not sufficient, as some internal metadata may remain. Workers with a compatibility date before [2026-02-24](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#durable-object-deleteall-deletes-alarms) and an alarm set should delete the alarm first with `deleteAlarm()`.

* [  JavaScript ](#tab-panel-5995)
* [  TypeScript ](#tab-panel-5996)

index.js

```

import { DurableObject } from "cloudflare:workers";


export class ChatRoom extends DurableObject {

  async clearStorage() {

    // Delete all storage, including any set alarm

    await this.ctx.storage.deleteAll();


    // The Durable Object instance still exists, but with empty storage

    // A subsequent request will find no data

  }

}


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  CHAT_ROOM: DurableObjectNamespace<ChatRoom>;

}


export class ChatRoom extends DurableObject<Env> {

  async clearStorage() {


      // Delete all storage, including any set alarm

      await this.ctx.storage.deleteAll();


      // The Durable Object instance still exists, but with empty storage

      // A subsequent request will find no data

    }

}


```

### Design for unexpected shutdowns

Durable Objects may shut down at any time due to deployments, inactivity, or runtime decisions. Rather than relying on shutdown hooks (which are not provided), design your application to write state incrementally.

Durable Objects may shut down due to deployments, inactivity, or runtime decisions. Rather than relying on shutdown hooks (which are not provided), design your application to write state incrementally.

Shutdown hooks or lifecycle callbacks that run before shutdown are not provided because Cloudflare cannot guarantee these hooks would execute in all cases, and external software may rely too heavily on these (unreliable) hooks.

Instead of relying on shutdown hooks, you can regularly write to storage to recover gracefully from shutdowns.

For example, if you are processing a stream of data and need to save your progress, write your position to storage as you go rather than waiting to persist it at the end:

JavaScript

```

// Good: Write progress as you go

async processData(data) {

  data.forEach(async (item, index) => {

    await this.processItem(item);

    // Save progress frequently

    await this.ctx.storage.put("lastProcessedIndex", index);

  });

}


```

While this may feel unintuitive, Durable Object storage writes are fast and synchronous, so you can persist state with minimal performance concerns.

This approach ensures your Durable Object can safely resume from any point, even if it shuts down unexpectedly.

## Anti-patterns to avoid

### Do not use a single Durable Object as a global singleton

A single Durable Object handling all traffic becomes a bottleneck. While async operations allow request interleaving, all synchronous JavaScript execution is single-threaded, and storage operations provide serialization guarantees that limit throughput.

A common mistake is using a Durable Object for global rate limiting or global counters. This funnels all traffic through a single instance:

* [  JavaScript ](#tab-panel-6009)
* [  TypeScript ](#tab-panel-6010)

index.js

```

import { DurableObject } from "cloudflare:workers";


// 🔴 Bad: Global rate limiter - ALL requests go through one instance

export class RateLimiter extends DurableObject {

  async checkLimit(ip) {

    const key = `rate:${ip}`;

    const count = (await this.ctx.storage.get(key)) ?? 0;

    await this.ctx.storage.put(key, count + 1);

    return count < 100;

  }

}


// 🔴 Bad: Always using the same ID creates a global bottleneck

export default {

  async fetch(request, env) {

    // Every single request to your application goes through this one DO

    const limiter = env.RATE_LIMITER.get(env.RATE_LIMITER.idFromName("global"));


    const ip = request.headers.get("CF-Connecting-IP") ?? "unknown";

    const allowed = await limiter.checkLimit(ip);


    if (!allowed) {

      return new Response("Rate limited", { status: 429 });

    }


    return new Response("OK");

  },

};


```

index.ts

```

import { DurableObject } from "cloudflare:workers";


export interface Env {

  RATE_LIMITER: DurableObjectNamespace<RateLimiter>;

}


// 🔴 Bad: Global rate limiter - ALL requests go through one instance

export class RateLimiter extends DurableObject<Env> {

  async checkLimit(ip: string): Promise<boolean> {

    const key = `rate:${ip}`;

    const count = (await this.ctx.storage.get<number>(key)) ?? 0;

    await this.ctx.storage.put(key, count + 1);

    return count < 100;

  }

}


// 🔴 Bad: Always using the same ID creates a global bottleneck

export default {

  async fetch(request: Request, env: Env): Promise<Response> {

    // Every single request to your application goes through this one DO

    const limiter = env.RATE_LIMITER.get(

      env.RATE_LIMITER.idFromName("global")

    );


    const ip = request.headers.get("CF-Connecting-IP") ?? "unknown";

    const allowed = await limiter.checkLimit(ip);


    if (!allowed) {

      return new Response("Rate limited", { status: 429 });

    }


    return new Response("OK");

  },

};


```

This pattern does not scale. As traffic increases, the single Durable Object becomes a chokepoint. Instead, identify natural coordination boundaries in your application (per user, per room, per document) and create separate Durable Objects for each.

## Testing and migrations

### Test with Vitest and plan for class migrations

Use `@cloudflare/vitest-pool-workers` for testing Durable Objects. The integration provides utilities for direct instance access.

* [  JavaScript ](#tab-panel-6013)
* [  TypeScript ](#tab-panel-6014)

test/chat-room.test.js

```

import { env } from "cloudflare:workers";

import { runInDurableObject, runDurableObjectAlarm } from "cloudflare:test";

import { describe, it, expect } from "vitest";


describe("ChatRoom", () => {

  it("should send and retrieve messages", async () => {

    const id = env.CHAT_ROOM.idFromName("test-room");

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


    // Call RPC methods directly on the stub

    await stub.sendMessage("user-1", "Hello!");

    await stub.sendMessage("user-2", "Hi there!");


    const messages = await stub.getMessages(10);

    expect(messages).toHaveLength(2);

  });


  it("can access instance internals and trigger alarms", async () => {

    const id = env.CHAT_ROOM.idFromName("test-room");

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


    // Access storage directly for verification

    await runInDurableObject(stub, async (instance, state) => {

      const count = state.storage.sql

        .exec("SELECT COUNT(*) as count FROM messages")

        .one();

      expect(count.count).toBe(2);

    });


    // Trigger alarms immediately without waiting

    const alarmRan = await runDurableObjectAlarm(stub);

    expect(alarmRan).toBe(false); // No alarm was scheduled

  });

});


```

test/chat-room.test.ts

```

import { env } from "cloudflare:workers";

import {

  runInDurableObject,

  runDurableObjectAlarm,

} from "cloudflare:test";

import { describe, it, expect } from "vitest";


describe("ChatRoom", () => {


it("should send and retrieve messages", async () => {

const id = env.CHAT_ROOM.idFromName("test-room");

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


      // Call RPC methods directly on the stub

      await stub.sendMessage("user-1", "Hello!");

      await stub.sendMessage("user-2", "Hi there!");


      const messages = await stub.getMessages(10);

      expect(messages).toHaveLength(2);

    });


    it("can access instance internals and trigger alarms", async () => {

      const id = env.CHAT_ROOM.idFromName("test-room");

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


      // Access storage directly for verification

      await runInDurableObject(stub, async (instance, state) => {

        const count = state.storage.sql

          .exec<{ count: number }>("SELECT COUNT(*) as count FROM messages")

          .one();

        expect(count.count).toBe(2);

      });


      // Trigger alarms immediately without waiting

      const alarmRan = await runDurableObjectAlarm(stub);

      expect(alarmRan).toBe(false); // No alarm was scheduled

    });

});


```

Configure Vitest in your `vitest.config.ts`:

TypeScript

```

import { cloudflareTest } from "@cloudflare/vitest-pool-workers";

import { defineConfig } from "vitest/config";


export default defineConfig({

  plugins: [

    cloudflareTest({

      wrangler: { configPath: "./wrangler.jsonc" },

    }),

  ],

});


```

For schema changes, run migrations in the constructor using `blockConcurrencyWhile()`. For class renames or deletions, use Wrangler migrations:

* [  wrangler.jsonc ](#tab-panel-5967)
* [  wrangler.toml ](#tab-panel-5968)

JSONC

```

{

  "migrations": [

    // Rename a class

    { "tag": "v2", "renamed_classes": [{ "from": "OldChatRoom", "to": "ChatRoom" }] },

    // Delete a class (removes all data!)

    { "tag": "v3", "deleted_classes": ["DeprecatedRoom"] }

  ]

}


```

TOML

```

[[migrations]]

tag = "v2"


  [[migrations.renamed_classes]]

  from = "OldChatRoom"

  to = "ChatRoom"


[[migrations]]

tag = "v3"

deleted_classes = [ "DeprecatedRoom" ]


```

Refer to [Durable Objects migrations](https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/) for more details on class migrations, and [Testing with Durable Objects](https://developers.cloudflare.com/durable-objects/examples/testing-with-durable-objects/) for comprehensive testing patterns including SQLite queries and alarm testing.

## Related resources

* [Workers Best Practices](https://developers.cloudflare.com/workers/best-practices/workers-best-practices/): code patterns for request handling, observability, and security that apply to the Workers calling your Durable Objects.
* [Rules of Workflows](https://developers.cloudflare.com/workflows/build/rules-of-workflows/): best practices for durable, multi-step Workflows — useful when combining Workflows with Durable Objects for long-running orchestration.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/durable-objects/","name":"Durable Objects"}},{"@type":"ListItem","position":3,"item":{"@id":"/durable-objects/best-practices/","name":"Best practices"}},{"@type":"ListItem","position":4,"item":{"@id":"/durable-objects/best-practices/rules-of-durable-objects/","name":"Rules of Durable Objects"}}]}
```
