Durable Objects
Cloudflare Durable Objects for stateful edge computing, covering constructor patterns, storage API, WebSocket support, alarm handlers, consistency guarantees, and use cases like rate limiting, collaboration, and game state.
You are an expert in Cloudflare Durable Objects, a primitive for strongly consistent, stateful computation at the edge. Each Durable Object is a single-threaded, globally unique instance with its own persistent storage and the ability to coordinate between requests. ## Key Points - **Global uniqueness**: Only one instance of a Durable Object with a given ID exists at any time. - **Single-threaded**: Requests to the same object are serialized (no concurrent access to storage). - **Strong consistency**: Reads always see the latest writes within the same object. - **Transactional storage**: Multiple reads and writes within a single `storage.transaction()` are atomic. - **Location affinity**: The object runs near the first client that creates it, and stays there. - **"Durable Object storage operation exceeded timeout"** — Keep transactions short. Avoid heavy computation inside `storage.transaction()`. - **"Durable Object has been evicted"** — Objects are evicted after ~10 seconds of inactivity. In-memory state is lost. Use `blockConcurrencyWhile` to reload state. - **High latency** — The object runs in one location. Clients far away see higher latency. Consider using KV for read caching on top of Durable Objects for writes. - **Migration errors** — Ensure the `[[migrations]]` tag in `wrangler.toml` is updated when adding, renaming, or removing Durable Object classes.
skilldb get cloudflare-workers-skills/Durable ObjectsFull skill: 406 linesDurable Objects — Cloudflare Workers
You are an expert in Cloudflare Durable Objects, a primitive for strongly consistent, stateful computation at the edge. Each Durable Object is a single-threaded, globally unique instance with its own persistent storage and the ability to coordinate between requests.
Core Philosophy
Overview
Durable Objects solve the problem that Workers alone cannot: coordination and strong consistency. Each Durable Object has a unique ID, runs in a single location, processes requests one at a time (single-threaded), and has transactional storage. This makes them ideal for rate limiting, collaborative editing, chat rooms, game state, counters, and any workload requiring serialized access to shared state.
Key guarantees
- Global uniqueness: Only one instance of a Durable Object with a given ID exists at any time.
- Single-threaded: Requests to the same object are serialized (no concurrent access to storage).
- Strong consistency: Reads always see the latest writes within the same object.
- Transactional storage: Multiple reads and writes within a single
storage.transaction()are atomic. - Location affinity: The object runs near the first client that creates it, and stays there.
Setup
wrangler.toml
[durable_objects]
bindings = [
{ name = "COUNTER", class_name = "Counter" },
{ name = "CHAT_ROOM", class_name = "ChatRoom" },
]
[[migrations]]
tag = "v1"
new_classes = ["Counter", "ChatRoom"]
Durable Object class
export interface Env {
COUNTER: DurableObjectNamespace;
CHAT_ROOM: DurableObjectNamespace;
}
export class Counter implements DurableObject {
private state: DurableObjectState;
private env: Env;
constructor(state: DurableObjectState, env: Env) {
this.state = state;
this.env = env;
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/increment") {
let count = (await this.state.storage.get<number>("count")) || 0;
count++;
await this.state.storage.put("count", count);
return Response.json({ count });
}
if (url.pathname === "/get") {
const count = (await this.state.storage.get<number>("count")) || 0;
return Response.json({ count });
}
return new Response("Not Found", { status: 404 });
}
}
Calling a Durable Object from a Worker
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Get a Durable Object by name (creates a deterministic ID)
const id = env.COUNTER.idFromName("global-counter");
// Or generate a random unique ID
// const id = env.COUNTER.newUniqueId();
// Or reconstruct from a stored hex string
// const id = env.COUNTER.idFromString("abc123...");
// Get a stub (reference) to the object
const stub = env.COUNTER.get(id);
// Forward the request to the Durable Object
return stub.fetch(request);
},
};
Storage API
Basic key-value operations
export class MyObject implements DurableObject {
constructor(private state: DurableObjectState, private env: Env) {}
async fetch(request: Request): Promise<Response> {
const storage = this.state.storage;
// Put a single value
await storage.put("key", "value");
// Put multiple values atomically
await storage.put(new Map([
["user:name", "Alice"],
["user:email", "alice@example.com"],
["user:role", "admin"],
]));
// Get a single value
const name = await storage.get<string>("user:name");
// Get multiple values
const entries = await storage.get<string>(["user:name", "user:email", "user:role"]);
// Returns: Map<string, string>
// Delete
await storage.delete("key");
await storage.delete(["key1", "key2"]);
// List all keys (with optional prefix, limit, and cursor)
const all = await storage.list();
// Returns: Map<string, unknown>
const userKeys = await storage.list({ prefix: "user:" });
const paginated = await storage.list({ prefix: "msg:", limit: 100, reverse: true });
// Delete all data
await storage.deleteAll();
return new Response("OK");
}
}
Transactions
async function transferCredits(
storage: DurableObjectStorage,
fromUser: string,
toUser: string,
amount: number
): Promise<boolean> {
return await storage.transaction(async (txn) => {
const fromBalance = (await txn.get<number>(`balance:${fromUser}`)) || 0;
const toBalance = (await txn.get<number>(`balance:${toUser}`)) || 0;
if (fromBalance < amount) {
// Transaction will be rolled back if we throw
return false;
}
await txn.put(`balance:${fromUser}`, fromBalance - amount);
await txn.put(`balance:${toUser}`, toBalance + amount);
return true;
});
}
In-memory state with blockConcurrencyWhile
Use blockConcurrencyWhile in the constructor to load state into memory before handling any requests:
export class GameRoom implements DurableObject {
private players: Map<string, Player> = new Map();
private gameState: GameState | null = null;
constructor(private state: DurableObjectState, private env: Env) {
// Load state into memory during initialization
state.blockConcurrencyWhile(async () => {
this.gameState = await state.storage.get<GameState>("gameState") ?? null;
const playerEntries = await state.storage.list<Player>({ prefix: "player:" });
for (const [key, value] of playerEntries) {
const id = key.replace("player:", "");
this.players.set(id, value);
}
});
}
async fetch(request: Request): Promise<Response> {
// this.players and this.gameState are guaranteed to be loaded
// ...
}
}
WebSocket Support
Durable Objects can accept WebSocket connections, making them ideal for real-time collaboration:
export class ChatRoom implements DurableObject {
private sessions: Map<WebSocket, { name: string }> = new Map();
constructor(private state: DurableObjectState, private env: Env) {}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/websocket") {
if (request.headers.get("Upgrade") !== "websocket") {
return new Response("Expected WebSocket", { status: 426 });
}
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
const name = url.searchParams.get("name") || "Anonymous";
await this.handleWebSocket(server, name);
return new Response(null, { status: 101, webSocket: client });
}
return new Response("Not Found", { status: 404 });
}
private async handleWebSocket(ws: WebSocket, name: string) {
// Accept the connection
ws.accept();
this.sessions.set(ws, { name });
// Broadcast join
this.broadcast(JSON.stringify({ type: "join", name }), ws);
// Send message history
const history = await this.state.storage.get<string[]>("messages") || [];
ws.send(JSON.stringify({ type: "history", messages: history.slice(-50) }));
ws.addEventListener("message", async (event) => {
const data = JSON.parse(event.data as string);
if (data.type === "message") {
const msg = { name, text: data.text, timestamp: Date.now() };
const serialized = JSON.stringify({ type: "message", ...msg });
// Persist to storage
const history = await this.state.storage.get<string[]>("messages") || [];
history.push(serialized);
if (history.length > 1000) history.splice(0, history.length - 1000);
await this.state.storage.put("messages", history);
// Broadcast to all connected clients
this.broadcast(serialized);
}
});
ws.addEventListener("close", () => {
this.sessions.delete(ws);
this.broadcast(JSON.stringify({ type: "leave", name }));
});
}
private broadcast(message: string, exclude?: WebSocket) {
for (const [ws] of this.sessions) {
if (ws !== exclude) {
try { ws.send(message); } catch { this.sessions.delete(ws); }
}
}
}
}
Hibernatable WebSockets
For long-lived connections, use the Hibernation API to avoid paying for idle CPU time:
export class HibernatableChat implements DurableObject {
constructor(private state: DurableObjectState, private env: Env) {}
async fetch(request: Request): Promise<Response> {
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
// Use acceptWebSocket instead of ws.accept()
this.state.acceptWebSocket(server, ["user-tag"]);
return new Response(null, { status: 101, webSocket: client });
}
// Called when a message arrives (object wakes from hibernation if needed)
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
const sockets = this.state.getWebSockets();
for (const socket of sockets) {
socket.send(typeof message === "string" ? message : new Uint8Array(message));
}
}
async webSocketClose(ws: WebSocket, code: number, reason: string) {
ws.close(code, reason);
}
}
Alarm Handlers
Alarms let a Durable Object schedule itself to run at a future time — useful for delayed processing, cleanup, and periodic tasks:
export class DelayedQueue implements DurableObject {
constructor(private state: DurableObjectState, private env: Env) {}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/schedule") {
const { task, delaySeconds } = await request.json<{ task: string; delaySeconds: number }>();
// Store the task
const tasks = (await this.state.storage.get<string[]>("tasks")) || [];
tasks.push(task);
await this.state.storage.put("tasks", tasks);
// Schedule alarm (only one alarm at a time per object)
const currentAlarm = await this.state.storage.getAlarm();
if (!currentAlarm) {
await this.state.storage.setAlarm(Date.now() + delaySeconds * 1000);
}
return Response.json({ scheduled: true });
}
return new Response("Not Found", { status: 404 });
}
// Called when the alarm fires
async alarm(): Promise<void> {
const tasks = (await this.state.storage.get<string[]>("tasks")) || [];
if (tasks.length > 0) {
const task = tasks.shift()!;
await processTask(task);
await this.state.storage.put("tasks", tasks);
// Schedule next alarm if more tasks remain
if (tasks.length > 0) {
await this.state.storage.setAlarm(Date.now() + 1000);
}
}
}
}
Use Case: Rate Limiter
export class RateLimiter implements DurableObject {
constructor(private state: DurableObjectState, private env: Env) {}
async fetch(request: Request): Promise<Response> {
const { key, limit, windowMs } = await request.json<{
key: string; limit: number; windowMs: number;
}>();
const now = Date.now();
const windowStart = now - windowMs;
// Get existing timestamps
let timestamps = (await this.state.storage.get<number[]>(`rl:${key}`)) || [];
// Remove expired entries
timestamps = timestamps.filter((t) => t > windowStart);
if (timestamps.length >= limit) {
const retryAfter = Math.ceil((timestamps[0] + windowMs - now) / 1000);
return Response.json(
{ allowed: false, remaining: 0, retryAfter },
{ status: 429, headers: { "Retry-After": retryAfter.toString() } }
);
}
// Record this request
timestamps.push(now);
await this.state.storage.put(`rl:${key}`, timestamps);
return Response.json({
allowed: true,
remaining: limit - timestamps.length,
});
}
}
Troubleshooting
- "Durable Object storage operation exceeded timeout" — Keep transactions short. Avoid heavy computation inside
storage.transaction(). - "Durable Object has been evicted" — Objects are evicted after ~10 seconds of inactivity. In-memory state is lost. Use
blockConcurrencyWhileto reload state. - High latency — The object runs in one location. Clients far away see higher latency. Consider using KV for read caching on top of Durable Objects for writes.
- Migration errors — Ensure the
[[migrations]]tag inwrangler.tomlis updated when adding, renaming, or removing Durable Object classes.
Install this skill directly: skilldb add cloudflare-workers-skills
Related Skills
Workers AI
Cloudflare Workers AI for running inference at the edge, covering supported models, text generation, embeddings, image generation, speech-to-text, AI bindings, and streaming responses.
Workers D1
Cloudflare D1 serverless SQLite database for Workers, covering schema management, migrations, queries, prepared statements, batch operations, local development, replication, backups, and performance optimization.
Workers Fundamentals
Cloudflare Workers runtime fundamentals including V8 isolates, wrangler CLI, project setup, local development, deployment, environment variables, secrets, and compatibility dates.
Workers KV
Cloudflare Workers KV namespace for globally distributed key-value storage, including read/write patterns, caching strategies, TTL, list operations, metadata, bulk operations, and the eventual consistency model.
Workers Patterns
Production patterns for Cloudflare Workers including queue consumers, cron triggers, email workers, browser rendering, Hyperdrive database connection pooling, Vectorize vector search, and the analytics engine.
Workers R2
Cloudflare R2 object storage with S3-compatible API, covering bucket operations, multipart uploads, presigned URLs, public buckets, lifecycle rules, event notifications, and cost optimization compared to S3.