Welcome!

Slide to unlock and explore

Slide to unlock

Command Palette

Search for a command to run...

0
Blog
PreviousNext

Durable Objects - Building Stateful Applications at the Edge

Master Cloudflare Durable Objects for building real-time collaborative applications, rate limiting, WebSockets, and distributed state management at the edge.

Introduction

Durable Objects represent a paradigm shift in how we build stateful applications at the edge. While traditional serverless functions are stateless, Cloudflare Durable Objects provide strong consistency, coordination, and persistent state with sub-millisecond latency across a globally distributed network.

This post explores how Durable Objects solve the state management problem for edge computing and why they're revolutionary for building real-time applications, collaborative tools, and distributed systems.

The State Management Problem

Traditional Serverless Limitations

Traditional serverless architectures face challenges when applications need state:

The Dilemma:

Stateless Functions + External Database = High Latency & Database Bottleneck
   ↓                        ↓
Fast execution          Slow reads/writes
                        Consistency issues
                        Cost scaling
  • Database Round Trips: Each state operation adds 50-200ms latency
  • Consistency Challenges: Distributed systems are hard to coordinate
  • Scaling Issues: Database becomes the bottleneck
  • Cost Multiplication: Database charges stack on top of compute costs

Why Edge State is Different

Durable Objects solve this by bringing state to the computation rather than computation to the state:

Traditional: Request → Function → Database → Response (200ms+)
Durable Objects: Request → Isolate with State → Response (1-5ms)

What Are Durable Objects?

Durable Objects are persistent, strongly consistent objects that live on Cloudflare's edge network. Each object:

  • Lives Globally: Automatically geo-routed to the nearest location
  • Maintains State: In-memory state with persistent storage
  • Provides Coordination: Strong consistency for shared state
  • Scales Instantly: No provisioning or cold starts
  • Costs Predictably: $0.15 per million requests + $5 per GB-month storage

Core Characteristics

FeatureTraditional DBDurable Objects
Latency50-200ms1 to 5ms
ConsistencyEventualStrong
Geo-distributionSingle regionGlobal edges
Cold startsN/ANever
CoordinationComplexBuilt-in
Cost per request$0.01-0.10$0.00015

How Durable Objects Work

The Architecture

User Request
    ↓
Nearest Cloudflare Edge
    ↓
Route to Durable Object (by ID)
    ↓
Object receives request
    ↓
Process with state
    ↓
Return response

Key Point: The Durable Object is always in the same location (routed by ID). All requests for the same object go to the same instance, ensuring strong consistency.

Creating Your First Durable Object

// durable_objects/counter.js
export class Counter {
  constructor(state, env) {
    this.state = state;
    this.env = env;
  }
 
  async fetch(request) {
    const url = new URL(request.url);
    const count = (await this.state.get('count')) || 0;
 
    if (request.method === 'POST') {
      const newCount = count + 1;
      await this.state.put('count', newCount);
      return new Response(JSON.stringify({ count: newCount }));
    }
 
    return new Response(JSON.stringify({ count }));
  }
}
 
export default Counter;

Binding in wrangler.toml

[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"
script_name = "my-worker"
 
[[env.production.durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"
 
[[migrations]]
tag = "v1"
new_classes = ["Counter"]

Real-World Use Cases

1. Real-Time Collaborative Editing

Imagine Google Docs but running at the edge:

export class Document {
  constructor(state, env) {
    this.state = state;
    this.connections = new Set();
  }
 
  async fetch(request) {
    // WebSocket support
    if (request.headers.get('Upgrade') === 'websocket') {
      const pair = new WebSocketPair();
      
      this.connections.add(pair[1]);
      pair[1].accept();
 
      // Handle incoming edits
      pair[1].addEventListener('message', async (msg) => {
        const edit = JSON.parse(msg.data);
        
        // Persist edit
        const edits = (await this.state.get('edits')) || [];
        edits.push(edit);
        await this.state.put('edits', edits);
 
        // Broadcast to all connections
        for (const conn of this.connections) {
          conn.send(JSON.stringify(edit));
        }
      });
 
      return new Response(null, { status: 101, webSocket: pair[0] });
    }
 
    // Get current document state
    const edits = (await this.state.get('edits')) || [];
    return new Response(JSON.stringify({ edits }));
  }
}

Performance Benefits:

  • Edits broadcast instantly (sub-millisecond latency)
  • No database round trips per keystroke
  • Strong consistency - all users see same version
  • Global users connected to nearest object instance

2. Rate Limiting & Quota Management

export class RateLimiter {
  constructor(state) {
    this.state = state;
  }
 
  async fetch(request) {
    const userId = request.headers.get('user-id');
    const key = `quota:${userId}`;
    
    const quota = (await this.state.get(key)) || {
      requests: 0,
      reset: Date.now() + 60000 // 1 minute
    };
 
    if (Date.now() > quota.reset) {
      quota.requests = 0;
      quota.reset = Date.now() + 60000;
    }
 
    if (quota.requests >= 100) {
      return new Response('Rate limited', { status: 429 });
    }
 
    quota.requests++;
    await this.state.put(key, quota);
 
    return new Response('OK', { status: 200 });
  }
}

Advantages:

  • Precise per-user rate limiting
  • No external service calls
  • Instant decision making
  • sub-millisecond overhead

3. Real-Time Analytics & Counters

export class Analytics {
  constructor(state) {
    this.state = state;
  }
 
  async fetch(request) {
    const { pathname } = new URL(request.url);
    const event = pathname.split('/')[1];
 
    if (request.method === 'POST') {
      // Increment counter
      const count = (await this.state.get(event)) || 0;
      await this.state.put(event, count + 1);
      
      return new Response('tracked');
    }
 
    // Get analytics
    const stats = {};
    const keys = await this.state.list();
    
    for (const key of keys.keys) {
      stats[key.name] = await this.state.get(key.name);
    }
 
    return new Response(JSON.stringify(stats));
  }
}

Performance:

  • Counters updated instantly (no batch delays)
  • Real-time analytics without database
  • Consistent reads and writes
  • Scales to millions of events per second

4. Session Management

export class SessionManager {
  constructor(state) {
    this.state = state;
  }
 
  async fetch(request) {
    const { pathname } = new URL(request.url);
    const sessionId = pathname.split('/')[1];
 
    if (request.method === 'POST') {
      const sessionData = await request.json();
      await this.state.put(`session:${sessionId}`, sessionData, {
        expiration: Math.floor(Date.now() / 1000) + 86400 // 24 hours
      });
      return new Response('session created');
    }
 
    const session = await this.state.get(`session:${sessionId}`);
    return new Response(JSON.stringify(session || {}));
  }
}

Benefits:

  • Sessions never lost (persistent storage)
  • Global availability
  • sub-millisecond lookup times
  • Automatic TTL-based cleanup

Architecture Patterns

Pattern 1: Master Coordination

Use a Durable Object as a master coordinator:

export class Coordinator {
  constructor(state, env) {
    this.state = state;
    this.env = env;
  }
 
  async fetch(request) {
    const { pathname } = new URL(request.url);
    
    if (pathname === '/lock') {
      const lockName = request.headers.get('lock-name');
      const locked = await this.state.get(`lock:${lockName}`);
      
      if (locked) {
        return new Response('locked', { status: 409 });
      }
      
      await this.state.put(`lock:${lockName}`, true, {
        expirationTtl: 30 // 30 second timeout
      });
      
      return new Response('acquired');
    }
 
    return new Response('coordinator ready');
  }
}

Pattern 2: Event Sourcing

export class EventLog {
  constructor(state) {
    this.state = state;
  }
 
  async fetch(request) {
    if (request.method === 'POST') {
      const event = await request.json();
      
      // Append to event log
      const events = (await this.state.get('events')) || [];
      events.push({
        ...event,
        timestamp: Date.now()
      });
      
      await this.state.put('events', events);
      return new Response(JSON.stringify(event));
    }
 
    // Get all events
    const events = (await this.state.get('events')) || [];
    return new Response(JSON.stringify(events));
  }
}

Pattern 3: Distributed Cache

export class DistributedCache {
  constructor(state, env) {
    this.state = state;
    this.env = env;
  }
 
  async fetch(request) {
    const key = request.headers.get('cache-key');
    
    const cached = await this.state.get(key);
    if (cached) {
      return new Response(cached);
    }
 
    // Fetch from origin
    const response = await fetch(this.env.ORIGIN_URL + key);
    const data = await response.text();
    
    // Cache for 1 hour
    await this.state.put(key, data, { expirationTtl: 3600 });
    
    return new Response(data);
  }
}

Performance Characteristics

Latency Comparison

Operation                  | Database | Durable Objects
---------------------------|----------|----------------
Single read                | 50 to 100ms | 1 to 2ms
Single write               | 50 to 100ms | 1 to 2ms
Rate limit check           | 50 to 100ms | less than 1ms
Transaction                | 100 to 300ms | 2 to 5ms
Broadcast to 1000 clients  | 5 to 10 seconds | 100 to 500ms

Consistency Model

Durable Objects guarantee:

  • Strong consistency: All operations are serialized
  • Atomicity: Operations complete fully or not at all
  • Durability: Data survives edge restarts
  • No race conditions: Request order is guaranteed

Storage and Persistence

In-Memory State

// Fast but limited
const data = await this.state.get('key');
await this.state.put('key', value);

Characteristics:

  • Sub-millisecond access
  • Lost if object is restarted
  • Limited by available RAM (128 MB per object)

Persistent Storage (durable_location)

// Survives restarts
await this.state.put('key', value); // Auto-persisted

Characteristics:

  • Automatically replicated
  • $5 per GB-month storage cost
  • Slightly higher latency (still under 10ms)
  • Survives edge failures

Choosing Storage

Use In-Memory State For:
- Cache data (can be regenerated)
- Session data (short-lived)
- WebSocket connections
- Real-time counters

Use Persistent Storage For:
- Financial transactions
- User account data
- Event logs
- Audit trails

Scaling Considerations

When to Use Multiple Objects

// Bad: Single object for all users
const obj = env.SHARED_OBJECT.get('global');
 
// Good: One object per user
const obj = env.USERS.get(userId);
 
// Good: One object per room/session
const obj = env.ROOMS.get(roomId);

Principle: Each Durable Object is a bottleneck. Distribute load across multiple objects by ID.

Users: 1,000 to 10,000 per object
Events: 1 to 10 million per second per object
Connections: 1,000 to 10,000 per object
State size: 1 to 50 MB per object

Cost Analysis

Pricing Breakdown

Durable Object Requests: $0.15 per million
Storage: $5 per GB-month
Example workload:
- 10 million requests/month
- 100 MB storage
= (10M × $0.15 / 1M) + (0.1 × $5)
= $1.50 + $0.50 = $2/month

Comparison with Databases

Traditional Database:
- Simple read: $0.00001 per request × 10M = $100
- Storage: $0.50 per GB × 0.1 GB = $50
- Total: $150/month

Durable Objects:
- 10M requests: $1.50
- 100MB storage: $0.50
- Total: $2/month

Savings: 75x cheaper!

Best Practices

1. Design for Locality

// Route requests to nearest object
const userId = request.headers.get('user-id');
const obj = env.USERS.get(userId); // Consistent routing
 
return obj.fetch(request);

2. Handle Errors Gracefully

export class SafeObject {
  async fetch(request) {
    try {
      const data = await this.state.get('key');
      return new Response(JSON.stringify(data));
    } catch (e) {
      // Fallback behavior
      return new Response(
        JSON.stringify({ error: 'Service unavailable' }),
        { status: 503 }
      );
    }
  }
}

3. Implement Alarms for Cleanup

export class CleanupObject {
  constructor(state, env) {
    this.state = state;
    this.env = env;
  }
 
  async alarm() {
    // Cleanup expired data
    const now = Date.now();
    const keys = await this.state.list();
    
    for (const key of keys.keys) {
      const item = await this.state.get(key.name);
      if (item.expiration < now) {
        await this.state.delete(key.name);
      }
    }
  }
}

4. Monitor Object Usage

// Track metrics
const requests = (await this.state.get('metrics:requests')) || 0;
await this.state.put('metrics:requests', requests + 1);
 
// Set alarms for high usage
if (requests > 1000000) {
  // Consider splitting object
}

Advanced Features

WebSocket Support

// Handle real-time bidirectional communication
const pair = new WebSocketPair();
pair[1].accept();
 
// Echo server
pair[1].addEventListener('message', msg => {
  pair[1].send(msg.data);
});
 
return new Response(null, { status: 101, webSocket: pair[0] });

Scheduled Alarms

// Run tasks at specific times
export class ScheduledObject {
  async fetch(request) {
    const nextAlarm = new Date(Date.now() + 60000); // 1 minute
    await this.state.setAlarm(nextAlarm);
    
    return new Response('alarm scheduled');
  }
 
  async alarm() {
    // Runs automatically at scheduled time
    console.log('Alarm triggered!');
  }
}

Conclusion

Durable Objects revolutionize edge computing by solving the state management problem:

  1. Global Coordination: Consistent state across 300+ edge locations
  2. Instant Latency: sub-millisecond response times without database
  3. Strong Consistency: ACID-like guarantees built-in
  4. Cost Effective: 75x cheaper than traditional databases
  5. Scalable: Handle millions of operations per second
  6. Easy to Use: Simple JavaScript API with automatic persistence

Whether you're building collaborative apps, real-time systems, or distributed coordination, Durable Objects provide the infrastructure to build at scale with sub-millisecond latency.

Quick Start Checklist

  • Define your Durable Object class with fetch() and alarm() methods
  • Bind the object in wrangler.toml
  • Route requests by object ID for consistency
  • Use persistent storage for critical data
  • Monitor object usage and split if needed
  • Test WebSocket connections for real-time features
  • Deploy and scale instantly

Start building stateful edge applications with Durable Objects today!