Skip to main content

Overview

Durable Objects provide low-latency coordination and consistent storage for stateful applications. Each Durable Object has a unique ID and guaranteed single-instance execution.

What are Durable Objects?

Durable Objects are:
  • Stateful - Maintain in-memory state and persistent storage
  • Single-threaded - One instance per ID, no concurrency within an object
  • Globally distributed - Automatically placed close to users
  • Strongly consistent - All requests to an object see the same state

Use Cases

Real-time Collaboration

  • Collaborative editing (docs, whiteboards)
  • Multiplayer games
  • Chat rooms and messaging
  • Live cursors and presence

Coordination

  • Distributed locks
  • Leader election
  • Rate limiting per user
  • Sequential processing

State Management

  • User sessions
  • Shopping carts
  • Connection pooling
  • Cached aggregations

Real-time Features

  • WebSocket connections
  • Server-sent events
  • Live updates and notifications
  • Presence tracking

Migrations

Durable Objects use migrations to track class bindings and lifecycle.

Configuration

Define Durable Objects in your wrangler.json:
wrangler.json
{
  "durable_objects": {
    "bindings": [
      {
        "name": "COUNTER",
        "class_name": "Counter",
        "script_name": "my-worker"
      },
      {
        "name": "CHAT_ROOM",
        "class_name": "ChatRoom"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_classes": ["Counter"]
    },
    {
      "tag": "v2",
      "new_classes": ["ChatRoom"],
      "renamed_classes": [
        {"from": "OldCounter", "to": "Counter"}
      ]
    },
    {
      "tag": "v3",
      "deleted_classes": ["OldCounter"]
    }
  ]
}

Migration Types

Add a new Durable Object class:
{
  "tag": "v1",
  "new_classes": ["Counter", "ChatRoom"]
}
Creates new classes that can be instantiated.

Implementation

Basic Durable Object

Create a simple counter:
export class Counter {
  private value: number = 0;
  private state: DurableObjectState;

  constructor(state: DurableObjectState, env: Env) {
    this.state = state;
    // Restore state from storage
    this.state.blockConcurrencyWhile(async () => {
      this.value = (await this.state.storage.get('value')) || 0;
    });
  }

  async fetch(request: Request) {
    const url = new URL(request.url);

    switch (url.pathname) {
      case '/increment':
        this.value++;
        await this.state.storage.put('value', this.value);
        return new Response(String(this.value));

      case '/decrement':
        this.value--;
        await this.state.storage.put('value', this.value);
        return new Response(String(this.value));

      case '/get':
        return new Response(String(this.value));

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

Worker Integration

Access Durable Objects from your Worker:
export default {
  async fetch(request, env) {
    // Get or create a Durable Object instance
    const id = env.COUNTER.idFromName('global');
    const stub = env.COUNTER.get(id);

    // Forward request to the Durable Object
    const response = await stub.fetch(request);
    return response;
  }
};

export { Counter } from './counter';

ID Generation

Create deterministic IDs from strings:
// Same name always returns same ID
const id = env.COUNTER.idFromName('user-123');
const id2 = env.COUNTER.idFromName('user-123');
// id === id2 (same instance)
Use Cases:
  • User-specific objects
  • Room/channel IDs
  • Resource-based routing

Storage API

Key-Value Operations

export class MyDurableObject {
  constructor(private state: DurableObjectState, private env: Env) {}

  async fetch(request: Request) {
    const storage = this.state.storage;

    // Put a value
    await storage.put('key', 'value');

    // Put multiple values
    await storage.put({
      'key1': 'value1',
      'key2': 'value2',
      'key3': 'value3'
    });

    // Get a value
    const value = await storage.get('key');

    // Get multiple values
    const values = await storage.get(['key1', 'key2']);
    // Returns: Map { 'key1' => 'value1', 'key2' => 'value2' }

    // Delete a value
    await storage.delete('key');

    // Delete multiple values
    await storage.delete(['key1', 'key2']);

    // List keys
    const keys = await storage.list();
    // Returns: Map of all key-value pairs

    // List with options
    const filtered = await storage.list({
      prefix: 'user:',
      limit: 100,
      reverse: false
    });

    return new Response('OK');
  }
}

Transactions

Use transactions for atomic operations:
// Automatic transaction (recommended)
const result = await this.state.storage.transaction(async (txn) => {
  const balance = (await txn.get('balance')) || 0;
  const newBalance = balance + 100;
  await txn.put('balance', newBalance);
  return newBalance;
});

// Manual transaction control
await this.state.storage.put('key1', 'value1');
await this.state.storage.put('key2', 'value2');
// Both operations are atomic if no await between them

Alarms

Schedule future work:
export class AlarmObject {
  constructor(private state: DurableObjectState, private env: Env) {}

  async fetch(request: Request) {
    // Set an alarm for 1 hour from now
    const now = Date.now();
    await this.state.storage.setAlarm(now + 60 * 60 * 1000);

    return new Response('Alarm set');
  }

  async alarm() {
    // Called when alarm triggers
    console.log('Alarm triggered!');

    // Do work
    await this.performCleanup();

    // Optionally set another alarm
    await this.state.storage.setAlarm(Date.now() + 60 * 60 * 1000);
  }

  async performCleanup() {
    // Your cleanup logic
  }
}

WebSockets

Handle real-time connections:
export class ChatRoom {
  private sessions: Set<WebSocket> = new Set();

  constructor(private state: DurableObjectState, private env: Env) {
    // Accept WebSocket hibernation
    this.state.acceptWebSocketHibernation = true;
  }

  async fetch(request: Request) {
    if (request.headers.get('Upgrade') === 'websocket') {
      const pair = new WebSocketPair();
      const [client, server] = Object.values(pair);

      // Accept the WebSocket
      this.state.acceptWebSocket(server);
      this.sessions.add(server);

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

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

  async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
    // Broadcast to all connected clients
    for (const session of this.sessions) {
      if (session !== ws && session.readyState === WebSocket.OPEN) {
        session.send(message);
      }
    }
  }

  async webSocketClose(ws: WebSocket, code: number, reason: string) {
    this.sessions.delete(ws);
  }

  async webSocketError(ws: WebSocket, error: Error) {
    console.error('WebSocket error:', error);
    this.sessions.delete(ws);
  }
}

Lifecycle Handlers

export class MyDurableObject {
  constructor(private state: DurableObjectState, private env: Env) {
    // Initialize during construction
    this.state.blockConcurrencyWhile(async () => {
      // Load initial state
      const data = await this.state.storage.get('data');
      this.initializeWithData(data);
    });
  }

  // Handle HTTP requests
  async fetch(request: Request) {
    return new Response('OK');
  }

  // Handle scheduled alarms
  async alarm() {
    // Called when alarm triggers
  }

  // Handle WebSocket messages
  async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
    // Handle incoming message
  }

  async webSocketClose(ws: WebSocket, code: number, reason: string) {
    // Handle connection close
  }

  async webSocketError(ws: WebSocket, error: Error) {
    // Handle WebSocket errors
  }
}

Best Practices

State Management

  • Keep in-memory state small
  • Persist critical data to storage
  • Use blockConcurrencyWhile() for initialization
  • Clean up unused data regularly

Performance

  • Batch storage operations
  • Use transactions for atomicity
  • Minimize state.storage calls
  • Cache frequently accessed data

Migrations

  • Always add migration tags
  • Test migrations in preview first
  • Use renamed_classes to preserve data
  • Never skip migration tags

WebSockets

  • Enable hibernation for efficiency
  • Handle errors gracefully
  • Track connection state
  • Implement heartbeat/ping

Concurrency Control

Block Concurrency

// Block all requests during initialization
await this.state.blockConcurrencyWhile(async () => {
  // This code runs exclusively
  const data = await this.state.storage.get('data');
  this.initialize(data);
});

Input Gates

// Wait for condition before accepting requests
await this.state.waitUntil(this.initializationPromise);

Limits

  • Storage per object: 50 GB
  • CPU time per request: 30 seconds
  • Concurrent requests: Serialized (one at a time)
  • WebSockets per object: Unlimited
  • Alarm frequency: Minimum 30 seconds

Common Patterns

export class RateLimiter {
  private requests: number[] = [];

  async fetch(request: Request) {
    const now = Date.now();
    const windowMs = 60000; // 1 minute
    const limit = 100;

    // Remove old requests
    this.requests = this.requests.filter(t => t > now - windowMs);

    if (this.requests.length >= limit) {
      return new Response('Rate limit exceeded', { status: 429 });
    }

    this.requests.push(now);
    return new Response('OK');
  }
}
export class DistributedLock {
  private locked = false;
  private queue: Array<() => void> = [];

  async fetch(request: Request) {
    const url = new URL(request.url);

    if (url.pathname === '/acquire') {
      await this.acquire();
      return new Response('Locked');
    }

    if (url.pathname === '/release') {
      this.release();
      return new Response('Unlocked');
    }

    return new Response('Unknown', { status: 400 });
  }

  private async acquire() {
    if (!this.locked) {
      this.locked = true;
      return;
    }

    // Wait in queue
    await new Promise<void>(resolve => {
      this.queue.push(resolve);
    });
  }

  private release() {
    const next = this.queue.shift();
    if (next) {
      next();
    } else {
      this.locked = false;
    }
  }
}
export class ConnectionPool {
  private connections: Map<string, WebSocket> = new Map();

  async webSocketMessage(ws: WebSocket, message: string) {
    // Route message to specific connection
    const data = JSON.parse(message);
    const target = this.connections.get(data.to);

    if (target) {
      target.send(message);
    }
  }

  async webSocketClose(ws: WebSocket) {
    // Clean up on disconnect
    for (const [id, socket] of this.connections) {
      if (socket === ws) {
        this.connections.delete(id);
        break;
      }
    }
  }
}

Build docs developers (and LLMs) love