blog.dopana

Back

Durable Objects (DO) provide a stateful coordination layer for Cloudflare Workers. Each DO runs as a globally unique instance — retaining state with strong consistency guarantees.

flowchart LR
    subgraph EdgeWorkers["Stateless Workers (Global Edge)"]
        direction TB
        W_Tokyo["Worker Tokyo"]
        W_London["Worker London"]
        W_US["Worker US"]
    end

    subgraph DO_Instance["Durable Object (Globally Unique Instance)"]
        direction TB
        ID["ID: 'user-room-42'"]
        
        subgraph Memory["RAM (In-Memory State)"]
            State["Active Connections & State"]
        end
        
        subgraph Storage["Persistent Storage"]
            SQL[("SQLite / KV Storage")]
        end

        ID --> Memory
        Memory <--> Storage
    end

    W_Tokyo -->|&quot;All requests with same ID&lt;br/&gt;route to single instance&quot;| DO_Instance
    W_London -->|&quot;Strong Consistency&lt;br/&gt;&lpar;Zero race conditions&rpar;&quot;| DO_Instance
    W_US -->|&quot;Continuous state retention&quot;| DO_Instance

Workers vs Durable Objects#

flowchart TB
    subgraph Clients["Clients & Browsers"]
        C1["User A (WebSocket)"]
        C2["User B (WebSocket)"]
        C3["User C (HTTP REST)"]
    end

    subgraph Edge["Cloudflare Global Edge (Stateless Workers)"]
        W1["Worker (Edge Location 1)"]
        W2["Worker (Edge Location 2)"]
    end

    subgraph DO_Cluster["Durable Objects (Single Coordinated Instance per ID)"]
        subgraph DO1["Chat Room DO (ID: room-123)"]
            Mem["In-Memory State & Active WebSockets"]
            SQL[(&quot;Built-in SQLite / KV Storage&quot;)]
            Alarm["Alarms / Timers"]
            Mem <--> SQL
            Mem <--> Alarm
        end
        subgraph DO2["Chat Room DO (ID: room-456)"]
            Mem2["In-Memory State"]
            SQL2[(&quot;SQLite Storage&quot;)]
        end
    end

    C1 <-->|&quot;Edge Connection&quot;| W1
    C2 <-->|&quot;Edge Connection&quot;| W2
    C3 -->|&quot;Request&quot;| W1

    W1 <-->|&quot;Route by ID: room-123&quot;| DO1
    W2 <-->|&quot;Route by ID: room-123&quot;| DO1
    W1 -.->|&quot;Route by ID: room-456&quot;| DO2
WorkersDurable Objects
StatelessStateful
Multiple requests - multiple instancesSingle unique instance per ID
No in-memory state retentionRetains state in RAM + persistent storage
Infinite automatic horizontal scalingScale via sharding across multiple DOs
Distributed across 330+ locationsSingle global point of execution (supports migration)

Chat Room — Basic Example#

sequenceDiagram
    autonumber
    actor ClientA as User Alice
    actor ClientB as User Bob
    participant Worker as Stateless Worker Router
    participant DO as ChatRoom [Durable Object]

    Note over ClientA, DO: WebSocket Connection Initialization
    ClientA->>Worker: GET /?room=general&name=Alice (Upgrade: WebSocket)
    Worker->>DO: stub.fetch(request) [idFromName("general")]
    DO->>DO: server.accept(), save session "Alice"
    DO-->>ClientA: 101 Switching Protocols (WebSocket Connected)
    DO--)ClientB: Broadcast "Alice joined the chat"

    Note over ClientA, DO: Real-time Message Broadcast
    ClientA->>DO: WS Message: "Hello room!"
    DO->>DO: broadcast("Alice: Hello room!", sender=Alice)
    DO--)ClientB: WS Message: "Alice: Hello room!"

SQLite Storage — Built-in Storage Engine#

Each Durable Object comes equipped with its own dedicated SQLite storage:

flowchart TD
    Req["Request: GET /increment /get /reset"] --> Match{url.pathname}
    Match -->|&quot;/increment&quot;| Inc["storage.get('count')<br/>storage.put('count', count + 1)"]
    Match -->|&quot;/get&quot;| Get["storage.get('count')"]
    Match -->|&quot;/reset&quot;| Res["storage.put('count', 0)"]
    Match -->|Other| Err["404 Not Found"]
    
    Inc --> Resp["Return JSON count"]
    Get --> Resp
    Res --> Resp

SQL Queries With storage.sql#

Execute direct SQL queries inside Durable Objects:

flowchart TD
    Req["HTTP Request"] --> Init["Create table if not exists: CREATE TABLE IF NOT EXISTS todos (...)"]
    Init --> Router{HTTP Method}
    
    Router -->|&quot;GET&quot;| Q1["SELECT * FROM todos ORDER BY id DESC"]
    Router -->|&quot;POST&quot;| Q2["INSERT INTO todos (title) VALUES (?)"]
    Router -->|&quot;PUT&quot;| Q3["UPDATE todos SET completed = ? WHERE id = ?"]
    Router -->|&quot;DELETE&quot;| Q4["DELETE FROM todos WHERE id = ?"]
    Router -->|Other| Q5["405 Method Not Allowed"]

    Q1 --> Out1["JSON: List of todos"]
    Q2 --> Out2["201 Created: new todo"]
    Q3 --> Out3["JSON: &#123; success: true &#125;"]
    Q4 --> Out4["JSON: &#123; success: true &#125;"]

Alarms — Scheduled Tasks for DO#

sequenceDiagram
    autonumber
    actor User as User / Client
    participant DO as Reminder DO
    participant Storage as ctx.storage [Alarm Queue]
    participant Webhook as External Webhook

    Note over DO, Storage: 1. Schedule Timer / Alarm
    User->>DO: GET /?delay=5000
    DO->>Storage: setAlarm(futureTimestamp)
    DO-->>User: {"message": "Alarm set for 5000ms"}

    Note over Storage, Webhook: 2. Execution upon Expiry (Wake up DO)
    Storage-->>DO: Trigger alarm() handler
    DO->>Webhook: POST https://hooks.example.com/notify { event: 'alarm_fired' }
    DO->>Storage: setAlarm(nextHourTimestamp) [Recurring trigger]

Multiplayer Game Server#

flowchart TD
    WS["WebSocket Message Event"] --> EventType{msg.type}

    EventType -->|&quot;join&quot;| Join["1. Add player session to Map<br/>2. Initialize Player state: x=0, y=0<br/>3. Broadcast: type: 'players'"]
    EventType -->|&quot;move&quot;| Move["1. Update player.x += dx, player.y += dy<br/>2. Broadcast: type: 'move'"]
    EventType -->|&quot;shoot&quot;| Shoot["1. Calculate bullet vector & angle<br/>2. Broadcast: type: 'shoot'"]

    Close["WebSocket Close Event"] --> Leave["1. Remove player from Map & State<br/>2. Broadcast: type: 'leave'"]

    Join --> BroadcastAll["Broadcast updated state to all connected room players"]
    Move --> BroadcastAll
    Shoot --> BroadcastAll
    Leave --> BroadcastAll

Migration — Moving DO Across Releases#

Durable Objects support zero-downtime migrations between classes and schemas:

// wrangler.jsonc
{
  "durable_objects": {
    "bindings": [
      {
        "name": "COUNTER",
        "class_name": "Counter",
        "migration": "new_tag"
      }
    ]
  }
}
typescript

Migration types:

  • new_tag — New class tag
  • new_classes — Add multiple classes
  • rename — Rename existing class
  • transfer — Transfer data between namespaces

Pattern — Sharding#

A single Durable Object instance lives in one physical location at a time. To scale horizontally, shard keys across multiple DOs:

flowchart LR
    Req["Request: userId"] --> Formula["shardId = floor(userId / 1000)"]
    Formula --> ShardMap{Shard Router}
    
    ShardMap -->|&quot;userId: 0 - 999&quot;| S0["DO Instance: shard-0"]
    ShardMap -->|&quot;userId: 1000 - 1999&quot;| S1["DO Instance: shard-1"]
    ShardMap -->|&quot;userId: 2000 - 2999&quot;| S2["DO Instance: shard-2"]
    ShardMap -->|&quot;userId: N - N+999&quot;| Sn["DO Instance: shard-N"]

Conclusion#

Durable Objects address the state problem in serverless computing:

  • Real-time: WebSocket connections paired with globally consistent memory state
  • Game servers: Low-latency coordination and synchronization
  • Distributed Coordination: Global locks, leader election, rate limiting
  • Embedded SQLite: Full relational queries without maintaining external databases

Architecture pattern: Keep request handling stateless on Workers, and delegate stateful coordination to Durable Objects. Shard instances to achieve unbounded scale.

References#