# Cloudflare Durable Objects Reference Sheet

# 1\. Core Concept & Mental Model

**Durable Objects = Stateful Actors**

* Each DO instance has a globally unique ID
    
* One instance per ID anywhere in the world
    
* Combines compute + storage in one place
    
* Single-threaded (no race conditions!)
    

```javascript
// Basic pattern
const id = env.MY_DO.idFromName("room-123"); // Same ID globally
const stub = env.MY_DO.get(id); // Get client to talk to DO
const result = await stub.myMethod(); // Call method on DO
```

# 2\. Project Setup

## wrangler.jsonc Configuration

```json
{
  "durable_objects": {
    "bindings": [
      {
        "name": "CHAT_ROOM",
        "class_name": "ChatRoom"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["ChatRoom"]
    }
  ]
}
```

## Generate Types

```bash
npx wrangler types  # Always run after config changes
```

# 3\. Basic DO Class Structure

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

export class ChatRoom extends DurableObject<Env> {
  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);

    // Initialize schema on startup
    this.ctx.blockConcurrencyWhile(async () => {
      await this.initializeSchema();
    });
  }

  private async initializeSchema() {
    await this.ctx.storage.sql.exec(`
      CREATE TABLE IF NOT EXISTS messages (
        id TEXT PRIMARY KEY,
        content TEXT NOT NULL,
        timestamp INTEGER NOT NULL
      );
    `);
  }

  // RPC methods - called from Workers
  async getMessage(id: string) {
    return this.ctx.storage.sql
      .exec("SELECT * FROM messages WHERE id = ?", id)
      .one();
  }
}
```

# 4\. Storage Options (3 Types)

## In-Memory (Temporary, Fast)

```javascript
export class GameRoom extends DurableObject {
  constructor(ctx, env) {
    super(ctx, env);
    this.activePlayers = new Map(); // Lost on hibernation
    this.gameStatus = "waiting"; // Lost on hibernation
  }
}
```

## Key-Value Storage (Persistent)

```javascript
// Store simple data
await this.ctx.storage.put("gameSettings", { difficulty: "hard" });
await this.ctx.storage.put("playerCount", 5);

// Retrieve data
const settings = await this.ctx.storage.get("gameSettings");
const count = await this.ctx.storage.get("playerCount");
```

## SQLite Storage (Persistent, Queryable)

```javascript
// Insert
await this.ctx.storage.sql.exec(
  "INSERT INTO messages (id, content, timestamp) VALUES (?, ?, ?)",
  messageId,
  content,
  Date.now()
);

// Query with types
const messages =
  this.ctx.storage.sql.exec <
  Message >
  ("SELECT * FROM messages ORDER BY timestamp DESC LIMIT ?", 10).toArray();

// Single result
const message = this.ctx.storage.sql
  .exec("SELECT * FROM messages WHERE id = ?", id)
  .one();
```

# 5\. Calling DOs from Workers

## idFromName vs newUniqueId

```javascript
// idFromName - Same string = same DO (perfect for rooms)
const roomId = env.CHAT_ROOM.idFromName("lobby-1"); // Always same DO

// newUniqueId - Random ID (better performance, store ID somewhere)
const sessionId = env.USER_SESSION.newUniqueId(); // Random unique DO
```

## RPC Method Calls

```javascript
// In your Worker/React Router
export async function loader({ context, params }) {
  const id = context.cloudflare.env.CHAT_ROOM.idFromName(params.roomId);
  const room = context.cloudflare.env.CHAT_ROOM.get(id);

  // Call DO methods directly
  const messages = await room.getMessages(50);
  const stats = await room.getRoomStats();

  return { messages, stats };
}
```

# 6\. WebSocket Integration

## DO WebSocket Handler

```javascript
export class ChatRoom extends DurableObject {
  // Accept WebSocket connections
  async fetch(request: Request): Promise<Response> {
    const webSocketPair = new WebSocketPair();
    const [client, server] = Object.values(webSocketPair);

    // Use hibernation-friendly API
    this.ctx.acceptWebSocket(server);

    return new Response(null, {
      status: 101,
      webSocket: client, // Return to browser
    });
  }

  // Handle messages from browser
  async webSocketMessage(ws: WebSocket, message: string) {
    const data = JSON.parse(message);

    if (data.type === "join_room") {
      // Send recent messages to this user
      const messages = await this.getMessages(20);
      ws.send(JSON.stringify({ type: "room_joined", messages }));
    }
  }

  // Broadcast to all connected users
  async sendMessage(content: string) {
    // Save to database first
    await this.ctx.storage.sql.exec("INSERT INTO messages...", content);

    // Then broadcast to all WebSockets
    const sockets = this.ctx.getWebSockets();
    const messageData = JSON.stringify({ type: "new_message", content });

    sockets.forEach((socket) => {
      try {
        socket.send(messageData);
      } catch (error) {
        // Socket closed, ignore
      }
    });
  }
}
```

## Worker WebSocket Endpoint

```javascript
// routes/api/rooms/[roomId]/ws.ts
export async function loader({ context, params }) {
  const id = context.cloudflare.env.CHAT_ROOM.idFromName(params.roomId);
  const room = context.cloudflare.env.CHAT_ROOM.get(id);
  return room.fetch(request); // Forward to DO
}
```

# 7\. DO Lifecycle & Hibernation

## Lifecycle States

* **Active** → Processing requests
    
* **Idle (hibernatable)** → No timers, fetch calls, or standard WebSockets
    
* **Hibernated** → Asleep, WebSockets stay connected, no billing
    
* **Inactive** → Completely removed from memory
    

## Hibernation Rules

```javascript
// ❌ Prevents hibernation
setTimeout(() => {}, 1000); // Timer active
fetch("https://api.example.com"); // Fetch pending
server.addEventListener("message", handler); // Standard WebSocket

// ✅ Allows hibernation
this.ctx.acceptWebSocket(server); // Hibernation WebSocket API
// No timers, no pending fetches
```

## blockConcurrencyWhile Pattern

```javascript
constructor(ctx, env) {
  super(ctx, env);

  // Prevent requests until initialization is done
  this.ctx.blockConcurrencyWhile(async () => {
    // Load persistent state into memory
    this.gameState = await this.ctx.storage.get("gameState") || {};
    await this.initializeSchema();
  });
}
```

# 8\. Error Handling & Retries

## Error Properties

```javascript
try {
  const room = env.CHAT_ROOM.get(id);
  await room.sendMessage(content);
} catch (error) {
  if (error.retryable) {
    // Safe to retry with backoff
  }
  if (error.overloaded) {
    // DON'T retry - will make it worse
  }
  if (error.remote) {
    // Error from DO code or infrastructure
  }
}
```

## Retry Pattern

```javascript
async function callDOWithRetry(stub, method, args, maxAttempts = 3) {
  let attempt = 0;

  while (attempt < maxAttempts) {
    try {
      return await stub[method](...args);
    } catch (error) {
      if (!error.retryable || error.overloaded) throw error;

      const backoff = 100 * Math.pow(2, attempt);
      await new Promise((resolve) => setTimeout(resolve, backoff));
      attempt++;
    }
  }

  throw new Error("Max retries exceeded");
}
```

# 9\. React Router 7 Integration

## Preventing Race Conditions

```javascript
// In your route file
export function shouldRevalidate({ formData, defaultShouldRevalidate }) {
  const intent = formData?.get("intent");

  // Don't revalidate for real-time actions
  if (intent === "sendMessage") {
    return false; // WebSocket handles updates
  }

  return defaultShouldRevalidate;
}
```

## Loader + Action Pattern

```javascript
export async function loader({ context, params }) {
  const id = context.cloudflare.env.CHAT_ROOM.idFromName(params.roomId);
  const room = context.cloudflare.env.CHAT_ROOM.get(id);
  return await room.getMessages();
}

export async function action({ context, params, request }) {
  const formData = await request.formData();
  const content = formData.get("content");

  const id = context.cloudflare.env.CHAT_ROOM.idFromName(params.roomId);
  const room = context.cloudflare.env.CHAT_ROOM.get(id);
  await room.sendMessage(content);

  return { success: true };
}
```

# 10\. Common Patterns

## Multiple DO Classes

```javascript
// Different concerns = different DO classes
export class ChatRoom extends DurableObject {
  // Handles chat for one room
}

export class UserSession extends DurableObject {
  // Handles user presence/authentication
}

export class GameRoom extends DurableObject {
  // Handles game state for one match
}
```

## Room Management Pattern

```javascript
// Worker routes to specific rooms
export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    if (url.pathname.startsWith("/room/")) {
      const roomId = url.pathname.split("/")[2];
      const id = env.CHAT_ROOM.idFromName(roomId);
      const room = env.CHAT_ROOM.get(id);
      return room.handleRequest(request);
    }
  },
};
```

## Data Architecture Split

```text
D1 Database (Global):
├── users (accounts, profiles)
├── rooms (metadata, descriptions)
└── global_stats (leaderboards)

DO Storage (Per Instance):
├── messages (chat history)
├── active_users (live connections)
└── room_state (game state, etc.)

DO Memory (Temporary):
├── websocket_connections
├── pending_actions
└── cache_data
```

# 11\. Quick Troubleshooting

**DO not receiving requests?**

* Check wrangler.jsonc bindings
    
* Run `npx wrangler types` after config changes
    
* Verify migration is applied
    

**WebSocket not connecting?**

* Check WSS vs WS protocol (HTTPS needs WSS)
    
* Verify WebSocket endpoint forwards to DO
    
* Look for hibernation-breaking code
    

**Messages not persisting?**

* Ensure using [`this.ctx.storage`](http://this.ctx.storage)`.sql.exec()` not just memory
    
* Check if table schema is created in constructor
    
* Verify DO has SQLite backend in migrations
    

**Race condition/flickering?**

* Add `shouldRevalidate` to prevent loader revalidation
    
* Use WebSocket for real-time, form submission for persistence
