Cloudflare Durable Objects
Status: Production Ready ✅ Last Updated: 2025-10-22 Dependencies: cloudflare-worker-base (recommended) Latest Versions: wrangler@4.43.0+, @cloudflare/workers-types@4.20251014.0+ Official Docs: https://developers.cloudflare.com/durable-objects/
What are Durable Objects?
Cloudflare Durable Objects are globally unique, stateful objects that provide:
- Single-point coordination - Each Durable Object instance is globally unique across Cloudflare's network
- Strong consistency - Transactional, serializable storage (ACID guarantees)
- Real-time communication - WebSocket Hibernation API for thousands of connections per instance
- Persistent state - Built-in SQLite database (up to 1GB) or key-value storage
- Scheduled tasks - Alarms API for future task execution
- Global distribution - Automatically routed to optimal location
- Automatic scaling - Millions of independent instances
Use Cases:
- Chat rooms and real-time collaboration
- Multiplayer game servers
- Rate limiting and session management
- Leader election and coordination
- WebSocket servers with hibernation
- Stateful workflows and queues
- Per-user or per-room logic
Quick Start (10 Minutes)
Option 1: Scaffold New DO Project
npm create cloudflare@latest my-durable-app -- \
--template=cloudflare/durable-objects-template \
--ts \
--git \
--deploy false
cd my-durable-app
npm install
npm run dev
What this creates:
- Complete Durable Objects project structure
- TypeScript configuration
- wrangler.jsonc with bindings and migrations
- Example DO class implementation
- Worker to call the DO
Option 2: Add to Existing Worker
cd my-existing-worker
npm install -D @cloudflare/workers-types
Create a Durable Object class (src/counter.ts):
import { DurableObject } from 'cloudflare:workers';
export class Counter extends DurableObject {
async increment(): Promise<number> {
// Get current value from storage (default to 0)
let value: number = (await this.ctx.storage.get('value')) || 0;
// Increment
value += 1;
// Save back to storage
await this.ctx.storage.put('value', value);
return value;
}
async get(): Promise<number> {
return (await this.ctx.storage.get('value')) || 0;
}
}
// CRITICAL: Export the class
export default Counter;
Configure wrangler.jsonc:
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-22",
// Durable Objects binding
"durable_objects": {
"bindings": [
{
"name": "COUNTER", // How you access it: env.COUNTER
"class_name": "Counter" // MUST match exported class name
}
]
},
// REQUIRED: Migration for new DO class
"migrations": [
{
"tag": "v1", // Unique migration identifier
"new_sqlite_classes": [ // Use SQLite backend (recommended)
"Counter"
]
}
]
}
Call from Worker (src/index.ts):
import { Counter } from './counter';
interface Env {
COUNTER: DurableObjectNamespace<Counter>;
}
export { Counter };
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Get Durable Object stub by name
const id = env.COUNTER.idFromName('global-counter');
const stub = env.COUNTER.get(id);
// Call RPC method on the DO
const count = await stub.increment();
return new Response(`Count: ${count}`);
},
};
Deploy:
npx wrangler deploy
Durable Object Class Structure
Base Class Pattern
All Durable Objects MUST extend DurableObject from cloudflare:workers:
import { DurableObject } from 'cloudflare:workers';
export class MyDurableObject extends DurableObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// Optional: Initialize from storage
ctx.blockConcurrencyWhile(async () => {
// Load state before handling requests
this.someValue = await ctx.storage.get('someKey') || defaultValue;
});
}
// RPC methods (recommended)
async myMethod(): Promise<string> {
return 'Hello from DO!';
}
// Optional: HTTP fetch handler
async fetch(request: Request): Promise<Response> {
return new Response('Hello from DO fetch!');
}
}
// CRITICAL: Export the class
export default MyDurableObject;
Constructor Pattern
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env); // REQUIRED
// Access to environment bindings
this.env = env;
// this.ctx provides:
// - this.ctx.storage (storage API)
// - this.ctx.id (unique ID)
// - this.ctx.waitUntil() (background tasks)
// - this.ctx.acceptWebSocket() (WebSocket hibernation)
}
CRITICAL Rules:
- ✅ Always call
super(ctx, env)first - ✅ Keep constructor minimal - heavy work blocks hibernation wake-up
- ✅ Use
ctx.blockConcurrencyWhile()to initialize from storage before requests - ❌ Never use
setTimeoutorsetInterval- breaks hibernation (use alarms instead) - ❌ Don't rely only on in-memory state with WebSockets - persist to storage
Exporting the Class
// Export as default (required for Worker to use it)
export default MyDurableObject;
// Also export as named export (for type inference in Worker)
export { MyDurableObject };
In Worker:
// Import the class for types
import { MyDurableObject } from './my-durable-object';
// Export it so Worker can instantiate it
export { MyDurableObject };
interface Env {
MY_DO: DurableObjectNamespace<MyDurableObject>;
}
State API - Persistent Storage
Durable Objects provide two storage APIs depending on the backend:
- SQL API (SQLite backend) - Recommended
- Key-Value API (KV or SQLite backend)
Enable SQLite Backend (Recommended)
In wrangler.jsonc migrations:
{
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MyDurableObject"] // ← Use this for SQLite
}
]
}
Why SQLite?
- ✅ Up to 1GB storage (vs 128MB for KV backend)
- ✅ Atomic operations (deleteAll is all-or-nothing)
- ✅ SQL queries with transactions
- ✅ Point-in-time recovery (PITR)
- ✅ Synchronous KV API available too
SQL API
Access via ctx.storage.sql:
import { DurableObject } from 'cloudflare:workers';
export class MyDurableObject extends DurableObject {
sql: SqlStorage;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
// Create table on first run
this.sql.exec(`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
user TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_created_at ON messages(created_at);
`);
}
async addMessage(text: string, user: string): Promise<number> {
// Insert with exec (returns cursor)
const cursor = this.sql.exec(
'INSERT INTO messages (text, user, created_at) VALUES (?, ?, ?) RETURNING id',
text,
user,
Date.now()
);
const row = cursor.one<{ id: number }>();
return row.id;
}
async getMessages(limit: number = 50): Promise<any[]> {
const cursor = this.sql.exec(
'SELECT * FROM messages ORDER BY created_at DESC LIMIT ?',
limit
);
// Convert cursor to array
return cursor.toArray();
}
async deleteOldMessages(beforeTimestamp: number): Promise<void> {
this.sql.exec(
'DELETE FROM messages WHERE created_at < ?',
beforeTimestamp
);
}
}
SQL API Methods:
// Execute query (returns cursor)
const cursor = this.sql.exec('SELECT * FROM table WHERE id = ?', id)