Durable Objects

Manage stateful serverless applications by automating Cloudflare Durable Objects orchestration

Durable Objects is a community skill for building stateful serverless applications on Cloudflare, covering strongly consistent storage, real-time coordination, WebSocket management, actor-model patterns, and distributed state machines at the edge.

What Is This?

Overview

Durable Objects provides patterns for managing persistent state in Cloudflare Workers using the Durable Objects API. It covers strongly consistent storage where each object instance maintains its own transactional key-value store, real-time coordination with single-threaded execution that eliminates concurrency conflicts, WebSocket management where a Durable Object acts as a coordination hub for connected clients, actor-model programming with named objects that encapsulate state and behavior, and distributed state machines that manage workflow transitions with guaranteed consistency. The skill enables developers to build collaborative, real-time, and transactional applications on the edge without traditional database infrastructure.

Who Should Use This

This skill serves developers building real-time collaborative features like shared documents or chat rooms, teams implementing distributed counters, rate limiters, or coordination primitives, and engineers designing WebSocket-based applications on Cloudflare Workers.

Why Use It?

Problems It Solves

Serverless functions are stateless by design, requiring external databases for persistence. Distributed counters and rate limiters need atomic operations that eventually consistent stores cannot guarantee. Real-time collaboration requires a single coordination point for WebSocket connections to prevent conflicts. Traditional databases add latency to edge functions that need low-latency state access.

Core Highlights

Single-threaded execution guarantees no concurrent access conflicts per object. Transactional storage provides atomic read-write operations within each object. WebSocket hibernation reduces costs by sleeping objects between messages. Automatic placement locates objects near their most frequent callers.

How to Use It?

Basic Usage

// Durable Object: counter
export class Counter {
  state: DurableObjectState;

  constructor(
    state: DurableObjectState
  ) {
    this.state = state;
  }

  async fetch(
    request: Request
  ): Promise<Response> {
    const url = new URL(
      request.url);

    if (url.pathname ===
        '/increment') {
      let value: number =
        (await this.state
          .storage.get(
            'count')) ?? 0;
      value++;
      await this.state
        .storage.put(
          'count', value);
      return Response.json(
        { count: value });
    }

    if (url.pathname ===
        '/get') {
      const value =
        (await this.state
          .storage.get(
            'count')) ?? 0;
      return Response.json(
        { count: value });
    }

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

// Worker entry point
export default {
  async fetch(
    request: Request,
    env: { COUNTER:
      DurableObjectNamespace }
  ) {
    const id = env.COUNTER
      .idFromName('global');
    const stub = env.COUNTER
      .get(id);
    return stub.fetch(request);
  },
};

Real-World Examples

// Chat room with WebSockets
export class ChatRoom {
  state: DurableObjectState;
  sessions: WebSocket[] = [];

  constructor(
    state: DurableObjectState
  ) {
    this.state = state;
  }

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

      server.accept();
      this.sessions
        .push(server);

      server.addEventListener(
        'message', (event) => {
          for (const ws
              of this.sessions) {
            if (ws !== server) {
              ws.send(
                event.data
                  as string);
            }
          }
        });

      server.addEventListener(
        'close', () => {
          this.sessions =
            this.sessions
              .filter(
                (s) =>
                  s !== server);
        });

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

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

Advanced Tips

Use WebSocket hibernation API to reduce billing by letting Durable Objects sleep between messages while maintaining connections. Batch multiple storage operations with transaction blocks for atomicity. Use alarms for scheduling future work within a Durable Object without external cron triggers.

When to Use It?

Use Cases

Build a collaborative document editor where a Durable Object coordinates real-time edits from multiple users. Create a distributed rate limiter with per-key counters that enforce limits without race conditions. Implement a multiplayer game server where each room is a Durable Object managing player state and WebSocket connections.

Related Topics

Cloudflare Workers, actor model, WebSocket coordination, edge computing, and distributed state.

Important Notes

Requirements

Cloudflare Workers paid plan for Durable Objects access. Wrangler CLI configured with Durable Object bindings in wrangler.toml. TypeScript for type-safe Durable Object class definitions.

Usage Recommendations

Do: use named IDs from stable identifiers like user IDs or room names for consistent routing. Keep Durable Object storage operations small and frequent rather than storing large blobs. Use alarms instead of external schedulers for time-based logic within objects.

Don't: create unbounded numbers of Durable Objects when a shared object with key-based storage would suffice. Store large files in Durable Object storage when R2 is designed for object storage. Assume objects persist in memory indefinitely since they may be evicted between requests.

Limitations

Each Durable Object runs on a single thread limiting throughput per object instance. Storage is limited per object and not designed for large datasets. Objects are placed in one region based on access patterns and cannot be explicitly multi-region.