Single-writer stateful compute at the edge. Each Durable Object is a uniquely addressable instance with in-memory state, durable storage, and WebSocket support. The primitive behind real-time collaboration, game servers, and anything that needs strong consistency per entity.
Durable Objects solve the problem Workers can't: state that persists across requests and coordinates concurrent access. Each DO has a unique ID, a single location where it runs, and a transactional key-value storage API. Because there's exactly one instance per ID at any time, you never have to worry about race conditions — all requests to that ID are serialised.
The "durable" part: the object's storage survives restarts. The "object" part: it's a JavaScript class with methods you call over RPC or HTTP. Think of it as a micro-service with exactly one instance, pinned to a location, with built-in persistence.
The killer feature is WebSocket hibernation. A DO can accept thousands of WebSocket connections, and when there are no messages, it hibernates — you only pay for active time, not idle connections.
A DO is a class with a fetch() method. You access its transactional storage via this.ctx.storage. The binding in wrangler.toml lets Workers create and address DO instances.
# wrangler.toml [[durable_objects.bindings]] name = "COUNTER" class_name = "Counter" [[migrations]] tag = "v1" new_classes = ["Counter"] // Counter Durable Object export class Counter { constructor(ctx, env) { this.ctx = ctx; } async fetch(req) { let val = (await this.ctx.storage.get("count")) || 0; val++; await this.ctx.storage.put("count", val); return new Response(val.toString()); } }
Workers address DOs by name or unique ID. The idFromName() method maps a string to a deterministic ID — so "room:lobby" always routes to the same instance.
export default { async fetch(req, env) { const id = env.COUNTER.idFromName("global-counter"); const stub = env.COUNTER.get(id); return stub.fetch(req); } };
DOs can accept WebSocket connections and hibernate when idle. The webSocketMessage and webSocketClose handlers wake the object only when there's activity.
export class ChatRoom { async fetch(req) { const [client, server] = Object.values(new WebSocketPair()); this.ctx.acceptWebSocket(server); return new Response(null, { status: 101, webSocket: client }); } async webSocketMessage(ws, msg) { // Broadcast to all connected clients for (const conn of this.ctx.getWebSockets()) { conn.send(msg); } } }
A DO instance runs in a single data centre (usually near where it was first accessed). Requests from distant users cross the network to reach it. This is a feature for consistency, but a cost for latency.
All requests to one DO instance are serialised. If you need parallelism, shard across multiple DOs (e.g., one per user, one per room).
DO storage is more expensive than R2 or KV. Use it for hot, frequently-accessed state. Archive cold data elsewhere.
Renaming or deleting a DO class requires a migration in wrangler.toml. Forgetting this causes deploy failures.