Skip to main content
Durable Objects provide strongly consistent, coordinated state for your applications. Each Durable Object has unique, transactional storage and can coordinate between multiple clients in real-time.

Overview

Durable Objects provide:
  • Strong consistency guarantees
  • Transactional storage with the Storage API
  • Persistent state across requests
  • Real-time coordination via WebSockets
  • Alarms for scheduled execution
  • Optional hibernation for cost efficiency
Implementation: src/workerd/api/actor-state.h and actor-state.c++

Defining a Durable Object

Create a Durable Object class:
export class Counter {
  constructor(state, env) {
    this.state = state;
    this.env = env;
  }

  async fetch(request) {
    // Get current count
    let count = (await this.state.storage.get('count')) || 0;
    
    // Increment
    count++;
    
    // Store
    await this.state.storage.put('count', count);
    
    return new Response(count.toString());
  }
}

Accessing Durable Objects

Access Durable Objects through namespace bindings:
export default {
  async fetch(request, env) {
    // Get a Durable Object ID
    const id = env.COUNTER.idFromName('global');
    
    // Get the Durable Object stub
    const stub = env.COUNTER.get(id);
    
    // Call the Durable Object
    const response = await stub.fetch(request);
    
    return response;
  }
};

Durable Object IDs

ID from name

Derive a deterministic ID from a name:
// Same name always produces the same ID
const id = env.ROOM.idFromName('room-123');

ID from string

Parse an ID from its string representation:
const idString = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
const id = env.ROOM.idFromString(idString);

New unique ID

Generate a new unique ID:
// Create a new unguessable ID
const id = env.ROOM.newUniqueId();
const idString = id.toString();

// Store or share this ID string
return new Response(idString);
Source: src/workerd/api/samples/durable-objects-chat/chat.js:147

Storage API

The storage API provides transactional key-value storage:

Get

Retrieve values:
// Get single value
const value = await this.state.storage.get('key');

// Get multiple values
const values = await this.state.storage.get(['key1', 'key2', 'key3']);
console.log(values); // Map { 'key1' => value1, 'key2' => value2, ... }
Source: src/workerd/api/actor-state.h:54

Put

Store values:
// Put single value
await this.state.storage.put('key', 'value');

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

Delete

Remove values:
// Delete single key
await this.state.storage.delete('key');

// Delete multiple keys
await this.state.storage.delete(['key1', 'key2', 'key3']);

List

List stored keys:
// List all keys
const entries = await this.state.storage.list();
for (const [key, value] of entries) {
  console.log(key, value);
}

// List with options
const entries = await this.state.storage.list({
  start: 'user:',
  end: 'user;',
  prefix: 'user:',
  reverse: false,
  limit: 100
});
Source: src/workerd/api/actor-state.h:67

Transactions

Perform atomic operations:
class BankAccount {
  async transfer(from, to, amount) {
    await this.state.storage.transaction(async (txn) => {
      // Get balances
      const fromBalance = (await txn.get(from)) || 0;
      const toBalance = (await txn.get(to)) || 0;
      
      // Validate
      if (fromBalance < amount) {
        throw new Error('Insufficient funds');
      }
      
      // Update balances
      await txn.put(from, fromBalance - amount);
      await txn.put(to, toBalance + amount);
      
      // Transaction commits automatically if no error is thrown
    });
  }
}

Alarms

Schedule future execution:
export class Task {
  constructor(state, env) {
    this.state = state;
  }

  async fetch(request) {
    // Schedule alarm for 1 hour from now
    const now = Date.now();
    await this.state.storage.setAlarm(now + 3600000);
    
    return new Response('Alarm set');
  }

  async alarm() {
    // Called when alarm fires
    console.log('Alarm triggered!');
    
    // Perform scheduled task
    await this.performTask();
  }

  async performTask() {
    // Do work...
  }
}

Get alarm

Check current alarm time:
const alarmTime = await this.state.storage.getAlarm();
if (alarmTime !== null) {
  console.log('Alarm scheduled for:', new Date(alarmTime));
}
Source: src/workerd/api/actor-state.h:65

Delete alarm

Cancel a scheduled alarm:
await this.state.storage.deleteAlarm();

WebSocket coordination

Coordinate real-time communication:
export class ChatRoom {
  constructor(state, env) {
    this.state = state;
    this.sessions = [];
  }

  async fetch(request) {
    if (request.headers.get('Upgrade') !== 'websocket') {
      return new Response('Expected WebSocket', { status: 400 });
    }

    const pair = new WebSocketPair();
    const [client, server] = pair;

    // Accept the WebSocket
    server.accept();

    // Add to sessions
    this.sessions.push(server);

    // Handle messages
    server.addEventListener('message', event => {
      const message = event.data;
      
      // Broadcast to all sessions
      for (const session of this.sessions) {
        try {
          session.send(message);
        } catch (err) {
          // Session closed
        }
      }
    });

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

    return new Response(null, {
      status: 101,
      webSocket: client
    });
  }
}
Source: src/workerd/api/samples/durable-objects-chat/chat.js:213

Hibernatable WebSockets

Reduce memory usage with hibernation:
export class HibernatableRoom {
  constructor(state, env) {
    this.state = state;
    
    // Enable hibernation
    this.state.setHibernatableWebSocketEventTimeout(30_000);
  }

  async fetch(request) {
    const pair = new WebSocketPair();
    const [client, server] = pair;

    // Accept with hibernation support
    this.state.acceptWebSocket(server);

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

  // Called when a message is received
  async webSocketMessage(ws, message) {
    // Durable Object wakes up if hibernating
    ws.send(`Echo: ${message}`);
  }

  // Called when connection closes
  async webSocketClose(ws, code, reason, wasClean) {
    console.log('WebSocket closed:', code, reason);
  }

  // Called on error
  async webSocketError(ws, error) {
    console.error('WebSocket error:', error);
  }
}

Patterns

Singleton coordination

Use a single Durable Object for global state:
class GlobalCounter {
  constructor(state) {
    this.state = state;
  }

  async increment() {
    const count = (await this.state.storage.get('count')) || 0;
    const newCount = count + 1;
    await this.state.storage.put('count', newCount);
    return newCount;
  }
}

// Access the singleton
const id = env.COUNTER.idFromName('global');
const counter = env.COUNTER.get(id);

Per-user state

Store state for each user:
class UserSession {
  constructor(state, env) {
    this.state = state;
  }

  async fetch(request) {
    const session = await this.state.storage.get('session') || {};
    
    // Update last seen
    session.lastSeen = Date.now();
    await this.state.storage.put('session', session);
    
    return new Response(JSON.stringify(session));
  }
}

// Access per user
const id = env.SESSION.idFromName(`user:${userId}`);
const session = env.SESSION.get(id);

Rate limiting

Implement distributed rate limiting:
class RateLimiter {
  constructor(state) {
    this.state = state;
    this.nextAllowedTime = 0;
  }

  async fetch(request) {
    const now = Date.now() / 1000;
    this.nextAllowedTime = Math.max(now, this.nextAllowedTime);

    if (request.method === 'POST') {
      // Allow one request per 5 seconds
      this.nextAllowedTime += 5;
    }

    // Calculate cooldown with 20 second grace period
    const cooldown = Math.max(0, this.nextAllowedTime - now - 20);
    return new Response(cooldown.toString());
  }
}
Source: src/workerd/api/samples/durable-objects-chat/chat.js:451

Best practices

Durable Objects are designed for coordination, not bulk storage:
// Good: small coordinated state
await this.state.storage.put('counter', count);

// Bad: storing large files
await this.state.storage.put('video', hugeBuffer);
Ensure consistency with transactions:
await this.state.storage.transaction(async (txn) => {
  const val1 = await txn.get('key1');
  const val2 = await txn.get('key2');
  await txn.put('key1', val1 + 1);
  await txn.put('key2', val2 - 1);
});
Clean up resources when connections close:
server.addEventListener('close', () => {
  this.sessions = this.sessions.filter(s => s !== server);
  // Clean up other resources
});
Reduce memory usage with hibernatable WebSockets:
this.state.acceptWebSocket(server);
// Connection can hibernate when idle

Storage options

Control caching and consistency:
// Allow concurrent reads (eventual consistency)
const value = await this.state.storage.get('key', {
  allowConcurrency: true
});

// Bypass cache
const value = await this.state.storage.get('key', {
  noCache: true
});

// Allow unconfirmed writes (performance)
await this.state.storage.put('key', value, {
  allowUnconfirmed: true
});
Source: src/workerd/api/actor-state.h:42

Implementation details

Durable Objects are implemented in:
  • src/workerd/api/actor-state.h / .c++ - Storage API (1800+ lines)
  • src/workerd/api/actor.h / .c++ - Durable Object infrastructure
  • src/workerd/io/actor-cache.h / .c++ - LRU cache layer over storage
  • src/workerd/io/actor-sqlite.h / .c++ - SQLite-backed storage
The storage API provides transactional operations over an actor cache that sits in front of SQLite storage.

Build docs developers (and LLMs) love