Cloudflare Durable Objects — Stateful Edge Computing without Servers

Posted on: 5/5/2026 10:11:35 AM

Why Stateful at the Edge is Hard

Serverless functions (AWS Lambda, Cloudflare Workers, Vercel Edge Functions) are designed stateless — each request is an independent execution with no shared memory between invocations. When you need state (sessions, counters, game state, collaborative documents), the traditional solution is connecting to a centralized database — Redis, PostgreSQL, DynamoDB — hosted in a specific region.

The problem: a centralized database creates a latency bottleneck. A user in Singapore hits a Worker running at the Singapore edge, but that Worker must round-trip to a database in US-East to read/write state. Latency increases by 150-300ms per request, destroying the edge computing advantage.

The Core Problem

How do you achieve strong consistency + low latency + zero ops for stateful workloads at the edge? Cloudflare Durable Objects solve exactly this problem using the Actor Model.

What Are Durable Objects?

Durable Objects (DO) are globally unique, single-threaded compute instances running on Cloudflare's edge network. Each DO instance:

  • Has a unique ID globally — exactly one instance exists at any given time
  • Processes requests sequentially (single-threaded) — no race conditions
  • Has its own private persistent storage (SQLite or Key-Value, up to 10GB)
  • Runs at the location closest to the first user accessing it
  • Automatically hibernates when idle, wakes up on demand
10 GB SQLite storage / object
0 ms Coordination latency (single-threaded)
300+ Global edge locations
$0 Free tier (5GB storage)

Actor Model at the Edge — Architecture

Durable Objects implement the Actor Model — each entity in your system (user session, chat room, game match, document) is an independent actor with its own state, processing messages sequentially.

graph TB
    subgraph "Cloudflare Edge Network"
        W1[Worker - Singapore] --> DO1[DO: Room-abc
SQLite + WebSocket] W2[Worker - Tokyo] --> DO1 W3[Worker - Sydney] --> DO1 W4[Worker - London] --> DO2[DO: Room-xyz
SQLite + WebSocket] W5[Worker - Frankfurt] --> DO2 end U1[User Singapore] --> W1 U2[User Tokyo] --> W2 U3[User Sydney] --> W3 U4[User London] --> W4 U5[User Frankfurt] --> W5 style DO1 fill:#e94560,stroke:#fff,color:#fff style DO2 fill:#e94560,stroke:#fff,color:#fff style W1 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style W2 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style W3 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style W4 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style W5 fill:#f8f9fa,stroke:#e94560,color:#2c3e50

Workers at every edge location route requests to the correct DO instance — strong consistency guaranteed by single-threaded execution

Why Single-Threaded is an Advantage

In traditional distributed systems, you need distributed locks, optimistic concurrency control, or consensus protocols (Raft, Paxos) to ensure consistency. With DO, the architecture itself eliminates all race conditions — no mutexes needed, no complex transaction isolation levels, no "read-your-writes" logic.

SQLite Storage — A Private Database per Entity

Since April 2025, Cloudflare recommends the SQLite storage backend for all new DO classes. Each DO instance gets its own isolated SQLite database with up to 10GB capacity.

// wrangler.toml
[[durable_objects.bindings]]
name = "CHAT_ROOM"
class_name = "ChatRoom"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["ChatRoom"]
// ChatRoom Durable Object
export class ChatRoom extends DurableObject {
  constructor(ctx, env) {
    super(ctx, env);
    // Create table on first instantiation
    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 TEXT DEFAULT (datetime('now'))
      )
    `);
  }

  async addMessage(userId, content) {
    // Single-threaded: no locks or transaction isolation needed
    this.ctx.storage.sql.exec(
      `INSERT INTO messages (user_id, content) VALUES (?, ?)`,
      userId, content
    );

    // Broadcast to all WebSocket clients
    const recent = this.ctx.storage.sql.exec(
      `SELECT * FROM messages ORDER BY id DESC LIMIT 1`
    ).toArray();

    this.broadcast(JSON.stringify(recent[0]));
  }
}

Point-in-Time Recovery (PITR)

SQLite in DO supports restoring data to any point in time within the past 30 days. Particularly useful for collaborative editing applications that need document state rollback.

WebSocket Hibernation — Keep Connections, Save Money

One of the most important DO features is WebSocket Hibernation. When a DO receives no events for a period, it gets evicted from memory but WebSocket connections remain active on Cloudflare's infrastructure.

sequenceDiagram
    participant C as Client
    participant CF as Cloudflare Edge
    participant DO as Durable Object

    C->>CF: WebSocket Connect
    CF->>DO: acceptWebSocket()
    DO->>DO: Active state (processing messages)
    Note over DO: 30s with no events
    DO-->>CF: Hibernate (evicted from memory)
    Note over CF: WebSocket connections still active
    C->>CF: Send message
    CF->>DO: Wake up DO
    DO->>DO: deserializeAttachment()
    DO->>C: Process + respond

WebSocket Hibernation: DO sleeps but connections stay alive — you only pay when there's actual traffic

export class ChatRoom extends DurableObject {
  async fetch(request) {
    const pair = new WebSocketPair();
    const [client, server] = Object.values(pair);

    // Use acceptWebSocket instead of server.accept()
    // to enable hibernation
    this.ctx.acceptWebSocket(server);

    // Store metadata in attachment (persists through hibernation)
    server.serializeAttachment({
      joinedAt: Date.now(),
      userId: new URL(request.url).searchParams.get('userId')
    });

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

  // Called when DO wakes from hibernation
  async webSocketMessage(ws, message) {
    const meta = ws.deserializeAttachment();
    await this.addMessage(meta.userId, message);
  }

  async webSocketClose(ws) {
    ws.close();
  }
}

Alarms API — Scheduled Tasks per Entity

Alarms allow scheduling a DO to wake up at a specific future time. Each DO can have one active alarm at a time — setting a new alarm overwrites the previous one.

export class GameMatch extends DurableObject {
  async startMatch(players) {
    this.ctx.storage.sql.exec(
      `INSERT INTO matches (players, status, started_at)
       VALUES (?, 'active', datetime('now'))`,
      JSON.stringify(players)
    );

    // Auto-end match after 10 minutes
    await this.ctx.storage.setAlarm(Date.now() + 10 * 60 * 1000);
  }

  // Called when alarm fires
  async alarm() {
    this.ctx.storage.sql.exec(
      `UPDATE matches SET status = 'completed' WHERE status = 'active'`
    );
    this.broadcast(JSON.stringify({ event: 'match_ended' }));
  }
}

System Design: Real-time Chat with Durable Objects

Let's design a complete real-time chat system using DO — demonstrating how all features come together in production.

graph LR
    subgraph "Client Layer"
        A[Browser/Mobile App]
    end

    subgraph "Edge Layer - Cloudflare Workers"
        B[Auth Worker]
        C[Router Worker]
    end

    subgraph "Durable Objects Layer"
        D[DO: User-123
Online status + prefs] E[DO: Room-general
Messages + members] F[DO: Room-engineering
Messages + members] end subgraph "Storage" G[(SQLite per DO
Messages history)] H[(Cloudflare KV
Room directory)] end A -->|WebSocket| B B -->|Validate JWT| C C -->|Route to room| E C -->|Route to room| F E --> G F --> G C -.->|Lookup| H D --> G style D fill:#e94560,stroke:#fff,color:#fff style E fill:#e94560,stroke:#fff,color:#fff style F fill:#e94560,stroke:#fff,color:#fff style B fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50 style C fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50

Chat system architecture: each room is a DO instance with its own SQLite, WebSocket Hibernation reduces cost for idle rooms

Comparison with Traditional Architecture

AspectDurable ObjectsTraditional (Redis + WS Server)
ConsistencyStrong by design (single-threaded)Requires distributed locks / Redis WATCH
ScalingAutomatic — 1 instance / entityManual sharding, sticky sessions
LatencyEdge near user (sub-50ms)Centralized (100-400ms cross-region)
WebSocketNative + Hibernation (90% cost savings idle)Always-on, pay for idle connections
Storage10GB SQLite / entity, 30-day PITRRedis: in-memory (expensive), PostgreSQL: shared
OperationsZero — fully managed by CloudflareProvisioning, patching, monitoring, failover
Cold start~5ms (wake from hibernation)N/A (always-on) but you pay for idle

Advanced Patterns

1. Fan-out Pattern — Efficient Broadcasting

export class ChatRoom extends DurableObject {
  broadcast(message, excludeWs = null) {
    // getWebSockets() returns all connected clients
    // including those in hibernation state
    for (const ws of this.ctx.getWebSockets()) {
      if (ws !== excludeWs && ws.readyState === WebSocket.OPEN) {
        ws.send(message);
      }
    }
  }
}

2. Rate Limiting Pattern — Zero-storage Coordination

export class RateLimiter extends DurableObject {
  // In-memory only — no persistence needed
  requests = [];

  async checkLimit(limit = 100, windowMs = 60000) {
    const now = Date.now();
    this.requests = this.requests.filter(t => now - t < windowMs);

    if (this.requests.length >= limit) {
      return { allowed: false, retryAfter: windowMs - (now - this.requests[0]) };
    }

    this.requests.push(now);
    return { allowed: true, remaining: limit - this.requests.length };
  }
}

3. Distributed Counter — Atomic Increment without Locks

export class PageViewCounter extends DurableObject {
  async increment(pageId) {
    // Single-threaded guarantee: no race conditions
    const result = this.ctx.storage.sql.exec(
      `INSERT INTO counters (page_id, count) VALUES (?, 1)
       ON CONFLICT(page_id) DO UPDATE SET count = count + 1
       RETURNING count`,
      pageId
    ).toArray();
    return result[0].count;
  }
}

Free Tier — Get Started at Zero Cost

Since April 2025, Cloudflare opened Durable Objects to the Workers Free plan:

Free Plan Includes

  • SQLite-backed Durable Objects (SQLite required, no KV)
  • 5 GB total storage per account
  • Unlimited requests (within Workers Free limits: 100K requests/day)
  • WebSocket Hibernation
  • Alarms API

With this free tier, you can fully build and deploy real-time applications like chat, collaborative editing, and multiplayer games without spending anything on infrastructure.

When NOT to Use Durable Objects

Limitations to Know

  • Single-region per object: A DO runs at one location — if globally distributed users access the same object, distant users will experience higher latency
  • Not a relational database: No JOINs between DOs — each entity is isolated. Need cross-entity queries? Use D1 or Hyperdrive
  • 10GB limit per object: If a single entity needs more than 10GB storage, manual partitioning is required
  • Cold start from hibernation: ~5ms, but still exists — not suitable for ultra-low-latency trading systems

Roadmap & Future

April 2025
SQLite storage GA (10GB), Free tier opened to everyone
January 2026
SQLite storage billing begins, 30-day PITR available
April 2026 — Agents Week
Durable Object Facets: dynamic workers with isolated SQLite. Flagship Feature Flags built on DO + KV
Future
Outgoing WebSocket Hibernation, multi-region replication (eventual consistency mode), larger storage limits

Conclusion

Cloudflare Durable Objects solve one of edge computing's biggest challenges: state management. By applying the Actor Model — one single-threaded instance per entity with private storage — DO completely eliminates distributed locking, race conditions, and consistency headaches without sacrificing latency or creating ops burden.

With 10GB SQLite per object, WebSocket Hibernation saving 90% on idle connections, Alarms for scheduled work, and a free tier with 5GB storage — this is the ideal platform for real-time collaboration, multiplayer games, chat systems, and AI agent sessions at the edge.

References: