Durable Objects give each chat room, document, game match, auction, or user its own stateful server — exactly one instance, anywhere on Earth, with storage built in. It's the fastest way to ship real-time, multiplayer, and coordinated apps.
Free tier: 100,000 requests and 13,000 GB-s of duration per day. No credit card required.
1 instanceper thing, globally — guaranteed by the network
128 MB + SQLitememory and an embedded database per object
Millionsof objects per account — zero servers to manage
Three different “things,” three independent objects. Requests for the same thing always land on the same instance — from anywhere.
The 30-second version
What is a Durable Object, actually?
Most explanations start with “serverless compute with strongly consistent coordination semantics.” Let's not.
A Durable Object is a tiny stateful server that exists exactly once, globally, for each thing you care about.
One per chat room. One per document. One per user, auction, job, or AI-agent session. You never launch or patch these servers — you address them by name, and Cloudflare guarantees that the right one, the only one, answers. When nobody's using it, it goes to sleep. When a request arrives, it wakes in milliseconds with its data still there.
01
Give every “thing” its own server
A multiplayer game isn't one giant server — it's thousands of tiny ones, one per match. Here the unit of scale is the thing itself: ROOMS.idFromName("match-8812") creates-or-reuses exactly that match's object. First.
02
The server remembers
Each object carries memory and an embedded SQLite database in the same process. State isn't a network hop away in someone else's cluster — it's a local variable and a local table, right next to your code.
03
The network does the hard part
Every request for a thing — arriving at any of Cloudflare's 330+ city networks — routes to that thing's one object. Uniqueness, placement, failover, and wake-ups are the platform's job. Yours is the product.
If you remember one sentence: a Worker is a stateless function that can run anywhere; a Durable Object is a stateful actor that runs in exactly one place. That single difference deletes an entire category of infrastructure — locks, caches, pub/sub glue, and “who owns this row?” arguments.
Interactive
One object, no race conditions.
The hard part of stateful apps isn't storing data — it's keeping one version of the truth when everyone writes at once. Watch the same two edits land in a traditional stateless stack versus in a Durable Object. Then see where the object lives.
The usual way stateless + shared DB
Stateless workers + one shared database. Each request lands on some worker; state lives far away. Press “Send concurrent edits” to see what breaks.
The Durable Object way one object per thing
Every request for this document reaches the same object. Edits are handled one at a time, in arrival order, against state that's right there. No locks, no races.
Two users, 20 ms apart, editing the same line of “Q3-plan”.
Create the object near…
The first request for a new thing decides where its object starts — close to the first user. After that, everyone routes to that instance. Choose a region:
const id = env.DOC.idFromName("Q3-plan")
Routing log
waiting for the first request…
Under the hood, in plain language
Five ideas that make it work.
You have the mental model. Here's what's actually happening — each mechanic, the problem it deletes, and the code you'd otherwise have to write.
01
Global uniqueness — the object is the lock
Two requests, same thing, same instance. Always.
A Durable Object class defines a kind of thing. Names identify individual things. idFromName("room-42") hashes the name into an ID that maps to exactly one live instance — in every Cloudflare location, for the lifetime of the thing.
That's why the docs call a Durable Object a single point of coordination. You don't take a lock to guard shared state; being the only writer is the lock. Two users opening “room-42” from opposite sides of the planet are, provably, talking to the same process.
Uniqueness is enforced by the router, not by your discipline: there is no flag, region, or race condition that can produce a second instance of the same ID.
Same name, any region → same ID → same instance.
worker.ts — routing by name (the only “config” you need)
// Any Worker, anywhere, can reach the same object by name:
export default {
async fetch(req, env) {
const url = new URL(req.url);
const room = url.searchParams.get("room") ?? "lobby";
// Same name → same ID, in every region, forever.
const id = env.CHAT_ROOMS.idFromName(room);
const stub = env.CHAT_ROOMS.get(id);
// Forward the request to that room's one instance.
return stub.fetch(req);
},
};
02
Single-threaded execution — races can't happen
Events arrive one at a time. No mutexes, no interleaving.
Inside an object, the runtime delivers one event at a time. While your code runs — or while a storage write is in flight — no other event is delivered (Cloudflare calls these input gates). The next request, WebSocket message, or alarm waits its turn.
The consequence is bigger than it sounds: this.value++; await save(); is correct because nothing else can run between those lines. The entire taxonomy of read-modify-write races — the reason you'd reach for locks, version numbers, or transactions — doesn't apply inside an object.
Caveat for honesty: single-threaded also means one hot object has a throughput ceiling. The scale-out answer is more objects — one per thing — not bigger ones. See the honest comparison.
Events serialize through one gate — interleaving is impossible by construction.
counter.ts — a correct counter with zero locking
import { DurableObject } from "cloudflare:workers";
export class Counter extends DurableObject {
// In-memory state is safe: only one event runs at a time.
count = 0;
async increment(amount = 1) {
this.count += amount; // read-modify-write, no lock
await this.ctx.storage.put("count", this.count); // durable
return this.count;
}
}
03
Colocated storage — no network hop
The database lives in the same process as your code.
In a stateless stack, state lives somewhere else: a central Postgres, a Redis node, a cache tier. Every request pays a network round-trip to reach it — and pays again to stay consistent.
A Durable Object's storage is embedded in the object itself. Reads and writes are local calls from your handler into SQLite — no connection pool, no VPC, no “cache stampede.” When the object moves or restarts, its data moves with it.
You get two storage APIs on the same data: familiar SQL (tables, indexes, transactions) and a tiny key-value API for quick durable flags and blobs.
Same machine, same process: the “database call” never leaves the object.
storage.ts — SQL and KV, both local
export class DocStore extends DurableObject {
async save(docId: string, body: string) {
// SQL with parameters, indexes, transactions — in-process.
this.ctx.storage.sql.exec(
`INSERT INTO versions (doc_id, body, at)
VALUES (?, ?, datetime('now'))`,
docId, body,
);
}
async pin(key: string, value: unknown) {
// Or the KV-style API for simple durable state.
await this.ctx.storage.put(key, value);
}
}
04
WebSocket hibernation — sleep while connected
Sockets stay open while the object bills $0.
A chat room with 500 idle users shouldn't cost like a busy server. With the Hibernation API, an object with no incoming events is evicted from memory while its WebSockets stay connected — the Cloudflare network holds the sockets.
When the next message arrives, the object re-initializes in milliseconds, restyles its state from SQLite (or per-socket attachments), and carries on. While it sleeps, no duration (GB-s) charges accrue — you pay for messages and requests, not for holding connections open.
Even asleep, the object can answer protocol pings via setWebSocketAutoResponse() — zero wake-up, zero billing for keepalives.
Hibernate without dropping a single client.
hibernate.ts — the whole “fleet” trick
import { DurableObject } from "cloudflare:workers";
export class Room extends DurableObject {
async fetch(req: Request) {
const [client, server] = Object.values(new WebSocketPair());
// Hibernating accept — the object may sleep after this:
this.ctx.acceptWebSocket(server);
return new Response(null, { status: 101, webSocket: client });
}
// Runs only when a message actually arrives (object wakes if needed).
async webSocketMessage(ws: WebSocket, msg: string) {
const user = this.ctx.getWebSockets().indexOf(ws);
this.broadcast(`${user ?? "guest"}: ${msg}`);
}
broadcast(text: string) {
for (const ws of this.ctx.getWebSockets()) ws.send(text);
}
}
05
SQLite-backed state — a real database per thing
Schema, indexes, transactions — one per object.
Every object ships with its own SQLite database. That means CREATE TABLE, indexes, constraints, joins, and transactions — not just get/put. Because each thing has its own database, you get per-tenant isolation for free: tenant A's queries physically cannot touch tenant B's rows.
SQLite is the storage backend Cloudflare recommends for every new Durable Object class, and it's the only backend available on the Free plan — so what you prototype is what you run.
Storage is billed by what you use: rows read, rows written, and bytes stored — see pricing.
One class → many objects, each with an isolated database.
schema.ts — real SQL, once per object
export class ChatRoom extends DurableObject {
async init() {
this.ctx.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user TEXT NOT NULL,
body TEXT NOT NULL,
at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_messages_at ON messages(at);
`);
}
history(limit = 50) {
// Prepared statement, streamed rows — locally, in-process.
return this.ctx.storage.sql
.prepare("SELECT user, body, at FROM messages ORDER BY id DESC LIMIT ?")
.all(limit);
}
}
Where Durable Objects shine
Seven problems that stop hurting.
For each: where the stateless-workers-plus-Redis approach strains, what goes wrong as it grows — and what the Durable Object version looks like instead.
Real-time collaboration and multiplayer
Shared documents, whiteboards, and game sessions are the canonical Durable Object workload: several people mutating one live thing, everyone needing to see everyone else's changes instantly — and the result to be correct.
Where the old way strains
You bolt a CRDT library onto stateless workers and a pub/sub broker — then conflict resolution becomes the product.
Presence stored in Redis with TTLs flickers on reconnect; cursors and selections drift.
When one document gets hot, you're hand-sharding rooms and pinning users to shards.
The Durable Object way
One object per document is the single sequencer: edits apply in arrival order, so last-writer-wins is correct, not a compromise.
Presence is a Map in memory, broadcast on every change; the version history is a local SQLite table.
Popular doc? It's still one object — and the other million docs don't care.
collab.ts — the whole multiplayer core
export class Whiteboard extends DurableObject {
strokes: Stroke[] = []; // authoritative, in-memory
async fetch(req: Request) {
const url = new URL(req.url);
const [client, server] = Object.values(new WebSocketPair());
this.ctx.acceptWebSocket(server, [url.searchParams.get("user") ?? "guest"]);
server.send(JSON.stringify({ type: "init", strokes: this.strokes }));
return new Response(null, { status: 101, webSocket: client });
}
async webSocketMessage(ws: WebSocket, raw: string) {
const stroke = JSON.parse(raw);
this.strokes.push(stroke); // serialized: no two-at-once
this.ctx.storage.sql.exec(
"INSERT INTO strokes (id, points) VALUES (?, ?)",
stroke.id, JSON.stringify(stroke.points),
);
this.broadcastBut(ws, raw); // everyone else draws it live
}
broadcastBut(sender: WebSocket, text: string) {
for (const peer of this.ctx.getWebSockets()) if (peer !== sender) peer.send(text);
}
}
Chat and presence
A channel is a thing. Its history, its members, its typing indicators — all one consistent little world. Durable Objects make each channel a self-contained server that costs nothing when the conversation goes quiet.
Where the old way strains
Socket servers need sticky sessions and a pub/sub layer (usually Redis) to fan messages across pods.
History queries from every pod hammer one primary database; presence keys expire mid-sentence.
Idle channels still cost full servers — most chat rooms are asleep most of the time.
The Durable Object way
One object per channel: sockets, history (SQLite), and presence in one process. No broker.
Hibernation means 100,000 quiet rooms cost roughly their storage — not 100,000 anything-servers.
Cloudflare's own durable-chat template is this pattern, ready to fork.
Some things must be counted, reserved, or bid on by exactly one arbiter at a time. A rate-limit bucket, the last seat on flight 447, inventory for SKU-991, the bid sequence of an auction — each is a “thing” that wants a single brain.
Where the old way strains
INCR+EXPIRE in Redis works until a key gets hot, a node fails over, or your limit logic needs two keys to agree.
Preventing oversell means optimistic locking and retries — under load, retry storms; under race, two buyers win.
Auctions need a serializable order of bids. You end up operating a queue just to fake one.
The Durable Object way
The counter is the object. Single-threaded means atomic by construction — check-then-set can't interleave.
One object per SKU / flight / auction serializes reservations in arrival order. No retries by design.
Billing that scales with things, not with a always-on coordination cluster.
rate-limit.ts — a global rate limiter with no Redis
export class RateLimiter extends DurableObject {
limit: number;
windowStart = Date.now();
count = 0;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.limit = Number(ctx.id.name.split(":")[1] ?? 100); // "user-42:100"
}
// Called directly from any Worker via RPC — billed as one request.
async check(cost = 1): Promise<{ ok: boolean; remaining: number }> {
const now = Date.now();
if (now - this.windowStart > 60_000) { // new minute window
this.windowStart = now;
this.count = 0;
}
if (this.count + cost > this.limit) {
await this.ctx.storage.put("lastDenied", now);
return { ok: false, remaining: 0 };
}
this.count += cost; // atomic: one thread
await this.ctx.storage.put("count", this.count);
return { ok: true, remaining: this.limit - this.count };
}
}
// Worker side:
// const id = env.RATE_LIMITERS.idFromName(`user-42:${100}`);
// const { ok } = await env.RATE_LIMITERS.get(id).check();
WebSocket fan-out at scale
Live sports ticks, trading dashboards, multiplayer lobbies, notification streams — the shape is always “one source, thousands of screens.” Cloudflare's product claim for Durable Objects: thousands of clients per object, millions of objects per account.
Where the old way strains
Long-lived sockets fight serverless: you size a stateful fleet for peak connections and pay for it at 4 a.m.
Load balancers need sticky sessions; a deploy becomes a reconnect storm across every client.
Cross-pod fan-out means every message transits the broker — twice, sometimes three times.
The Durable Object way
Clients connect to the nearest of 330+ Cloudflare locations; the edge carries the long haul to the object.
Fan-out is a for loop over the object's own sockets — one hop, no broker topology.
Hibernating idle sockets cost no duration. Scale by adding rooms, not racks.
fanout.ts — push to everyone, from one place
export class LiveFeed extends DurableObject {
subscribers = new Set();
async fetch(req: Request) {
const [client, server] = Object.values(new WebSocketPair());
this.ctx.acceptWebSocket(server);
return new Response(null, { status: 101, webSocket: client });
}
// Any Worker (or another object, or a Queue consumer) can call this:
async publish(event: string) {
let sent = 0;
for (const ws of this.ctx.getWebSockets()) {
ws.send(event); // fan-out: one loop, one hop
sent++;
}
// Tag+subscribe fan-out: this.ctx.getWebSockets("esports") filters topics.
return sent;
}
// Keep the hot path cheap: answer pings without waking the object.
async webSocketMessage() {}
}
Queues and workflows
A job that must retry with backoff, resume after a crash, or run step n+1 only after step n — that's a thing with state and a timeline. Give it an object, and the object is the workflow engine. The built-in Alarms API is its clock.
Where the old way strains
Cron + “database as queue” + dead-letter table + idempotency keys — and a scheduler you babysit.
Multi-step workflows need a state machine in Redis, and every worker must agree on whose turn it is.
Delayed jobs (“retry in 10 minutes”) turn into polling loops or a second scheduling product.
The Durable Object way
One object per job or per execution: steps and cursor live in its SQLite; alarm() is the retry clock.
Alarms survive sleep and restart, and the runtime retries them with backoff automatically.
Pairs naturally with Cloudflare Workflows for cross-object orchestration.
Multi-tenant SaaS usually means one shared schema where every row carries tenant_id and every query filters on it. Durable Objects offer the other shape: each tenant gets a whole tiny database of their own — isolated by physics, not by convention.
Where the old way strains
Row-level security, per-tenant indexes, and one noisy tenant degrading everyone's queries.
Schema migrations must be perfectly backward-compatible for all tenants at once.
“Delete all data for tenant X” becomes a multi-table, multi-index scavenger hunt.
The Durable Object way
One object per tenant = one SQLite database per tenant. No query can cross tenants — there's no path.
A hot tenant is one busy object; neighbors never notice. Placement even follows the tenant's geography.
Offboarding is deleting the object. Data residency becomes a routing decision.
tenant.ts — isolation by construction
// The entire multi-tenant routing layer:
export default {
async fetch(req, env) {
const tenant = new URL(req.url).host.split(".")[0]; // acme.app.example.com
// One object — one database — per tenant. That's the whole model.
const id = env.TENANT_DB.idFromName(tenant);
return env.TENANT_DB.get(id).fetch(req);
},
};
export class TenantDB extends DurableObject {
async query(sql: string, params: unknown[]) {
// No WHERE tenant_id = ? — this database *is* the tenant's.
return [...this.ctx.storage.sql.exec(sql, ...params)];
}
}
AI agent state
An agent is a thing with a memory, a conversation, a set of tools, and a timeline: it thinks, waits for a human, wakes when they reply, and must remember everything when it does. That's not a stateless function — that's an actor with storage and alarms. It's also exactly what Cloudflare's Agents SDK builds on Durable Objects.
Where the old way strains
Session state scattered across Redis (context), object storage (artifacts), and a scheduler (wake-ups) — three systems to keep in sync.
“Resume this conversation next Tuesday” becomes bespoke cron + queues + idempotency glue.
Long tool-call loops time out on request-scoped runtimes; the agent's continuity dies with the request.
The Durable Object way
One object per agent (or per user-agent session): memory in SQLite, live socket to the human, alarms for delayed turns.
Hibernation covers the “waiting for the user for three days” case — the agent sleeps, the socket stays, the bill doesn't run.
Background thinking between turns is just code after the response — the object is the runtime.
agent.ts — memory, wake-ups, and a live socket
export class SupportAgent extends DurableObject {
memory() {
return this.ctx.storage.sql.exec(
"SELECT role, content FROM turns ORDER BY id",
);
}
async userTurn(text: string) {
this.ctx.storage.sql.exec(
"INSERT INTO turns (role, content) VALUES ('user', ?)", text,
);
const reply = await this.think([...this.memory()]); // model call
this.ctx.storage.sql.exec(
"INSERT INTO turns (role, content) VALUES ('agent', ?)", reply,
);
this.push(reply); // live, if attached
return reply;
}
// "Follow up tomorrow at 9am if the ticket is still open."
async schedule(delayMs: number) {
await this.ctx.storage.setAlarm(Date.now() + delayMs);
}
async alarm() {
const open = await this.ctx.storage.get("ticketOpen");
if (open) this.push("Checking in — did that fix work?");
}
push(text: string) {
for (const ws of this.ctx.getWebSockets()) ws.send(text);
}
}
Developer experience
It reads like a class, because it is one.
No topology YAML, no broker configuration, no connection-pool tuning. You write a TypeScript (or Python) class with a fetch handler and storage on this.ctx — then ship it with one command.
Scaffold the project
Wrangler — Cloudflare's CLI — generates a working Durable Objects starter with tests and types included.
npm create cloudflare@latest my-app
Write the class
Extend DurableObject. Handle requests in fetch(), sockets in webSocketMessage(), persistence with this.ctx.storage. That's the API surface.
export class Chat extends DurableObject { … }
Deploy to the planet
One command pushes your Worker, its bindings, and the Durable Object classes it declares. Objects appear as traffic asks for them.
npx wrangler deploy
Prefer higher-level abstractions? The open-source @cloudflare/actors library adds actor-style patterns (entities, singletons, scheduling) on top — and the Cloudflare Agents SDK builds full agent runtime on the same primitive.
a counter — the “hello world” of state
import { DurableObject } from "cloudflare:workers";
interface Env {
COUNTERS: DurableObjectNamespace;
}
export class Counter extends DurableObject {
async fetch(request: Request): Promise {
let current = (await this.ctx.storage.get("value")) ?? 0;
const url = new URL(request.url);
// Update the value based on the request path.
switch (url.pathname) {
case "/increment":
current += 1;
await this.ctx.storage.put("value", current);
break;
case "/decrement":
current -= 1;
await this.ctx.storage.put("value", current);
break;
}
// Return the current value as a plain string.
return new Response(String(current));
}
}
export default {
async fetch(request: Request, env: Env) {
// Route to a unique instance based on the request path:
// /counter/foo and /counter/bar are two independent counters.
const name = new URL(request.url).pathname.split("/").pop() ?? "default";
const id = env.COUNTERS.idFromName(name);
return env.COUNTERS.get(id).fetch(request);
},
};
a WebSocket chat room — with hibernation
import { DurableObject } from "cloudflare:workers";
export class Chat extends DurableObject {
async fetch(request: Request): Promise {
if (request.headers.get("Upgrade") !== "websocket") {
return new Response("expected websocket", { status: 400 });
}
// Create the two ends of the WebSocket pair.
const [client, server] = Object.values(new WebSocketPair());
// Hibernation: the object may sleep while sockets stay open.
this.ctx.acceptWebSocket(server);
return new Response(null, { status: 101, webSocket: client });
}
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
if (typeof message !== "string") return;
// Durable state: history lives in the object's SQLite.
this.ctx.storage.sql.exec(
"INSERT INTO messages (body) VALUES (?)",
message,
);
// Fan out to every connected member of this room.
this.broadcast(message);
}
async webSocketClose(ws: WebSocket) {
this.broadcast("* user left the room *");
}
broadcast(message: string) {
for (const socket of this.ctx.getWebSockets()) {
socket.send(message);
}
}
}
RPC instead of HTTP — call your object like an object
import { DurableObject } from "cloudflare:workers";
export class RateLimiter extends DurableObject {
count = 0;
windowStart = Date.now();
// No fetch(), no routing, no serialization — just a method.
async check(limit = 100): Promise<{ ok: boolean; remaining: number }> {
if (Date.now() - this.windowStart > 60_000) {
this.windowStart = Date.now();
this.count = 0;
}
const ok = this.count < limit;
if (ok) this.count++;
await this.ctx.storage.put("count", this.count);
return { ok, remaining: Math.max(0, limit - this.count) };
}
}
export default {
async fetch(_req, env: Env) {
// Type-safe RPC from any Worker — billed as one request.
const id = env.RATE_LIMITERS.idFromName("api:user-42");
const result = await env.RATE_LIMITERS.get(id).check(100);
return Response.json(result);
},
};
Also available in Python (currently in beta) — same classes, same storage, same deploy.
Honest comparison
Durable Objects vs. the stack you'd otherwise build.
The conventional answer is stateless workers plus a database (and usually Redis). The ambitious answer is your own stateful fleet. Both genuinely work — here's what each costs you, and where Durable Objects are the wrong tool.
Stateless workers + DB/Redis
DIY stateful fleet
Durable Objects
Where state lives
A network hop away: central Postgres/Redis, plus cache tiers to soften it.
On machines you own and replicate — your ops problem now.
Inside the object: memory + embedded SQLite in one process.
Two users, one entity the classic race
Optimistic concurrency: locks, version columns, retries — correct eventually, with engineering.
Whatever you built. Some fleets pin entities to nodes; most discover races in production.
Serialized by a single thread. The race cannot occur.
WebSockets
Bolted on: sticky sessions, a gateway product, or both.
Native, but you size the fleet for peak connections and pray at deploy time.
Native, with hibernation: idle sockets stay connected at ~zero cost.
Idle cost
Database and cache run 24/7; per-connection servers idle at full price.
The fleet you sized for Friday night runs all Sunday.
Hibernating objects bill no duration — you pay for storage and actual activity.
Scaling model
Scale the workers — then shard the hot entity anyway, by hand.
Scale by having more things. Each object stays simple; nothing is rebalanced.
Ops burden
Manage DB failover, capacity, and the cache-invalidation folklore.
All of it: patching, autoscaling groups, load balancers, on-call.
Zero servers. Placement, failover, and wake-up are the platform's.
Single-entity ceiling
Scales far — that's the point (with eventual consistency as the price).
Scales if you engineer it to.
One object is single-threaded: a very hot single thing has a cap. The pattern is more objects, not bigger ones.
Time to working prototype
Days to weeks: schema, locking, cache, broker.
Weeks — infrastructure before product.
Minutes: one class, one binding, one deploy.
When Durable Objects are the wrong tool
A honest page says this out loud. Reach elsewhere when:
Big blobs and media
Files, images, video, model weights: store them in R2 (S3-compatible, zero egress fees) and keep only references in the object.
One shared relational database
When many callers need ad-hoc SQL across all entities at once, that's D1 or your existing warehouse — not thousands of per-thing databases.
Long, CPU-heavy work
Sustained number-crunching beyond Worker CPU limits belongs in Containers — which can still use a Durable Object as its coordinator and state.
Pricing & availability
Pay for activity, not for idle servers.
Durable Objects run on the Workers Free and Workers Paid plans. Hibernation means quiet things cost almost nothing — the bill follows traffic, not fleet size.
Workers Free
$0/ month, forever
100,000 requests / day (resets 00:00 UTC)
13,000 GB-s duration / day — idle objects don't consume it
5 GB SQLite storage included
SQLite-backed classes (the recommended, current backend)
Duration (wall-clock while active; 128 MB memory basis)
13,000 GB-s / day
400,000 GB-s / month included, then $12.50 / million GB-s
SQL rows read
—
$0.001 / million
SQL rows written
—
$1.00 / million
SQLite data stored
5 GB included
$0.20 / GB-month
Billing details worth knowing: incoming WebSocket messages are billed at a 20:1 ratio (small real-time messages), outgoing messages and protocol pings are free, and auto-response pings via setWebSocketAutoResponse() don't wake the object. Hibernating objects accrue no duration charges — that's the point of hibernation. Daily free limits reset at 00:00 UTC. Full detail, with worked examples, lives in the pricing documentation.
Social proof & ecosystem
Teams already run their real-time products this way.
Cloudflare released Durable Objects at just the right time for us. Without Cloudflare, hosting WebSocket servers might have required at least four additional people just for management. Using Durable Objects, we can provide serverless capabilities without a dedicated team.
No. It's serverless: you declare a class in your Worker project and deploy. Cloudflare places instances, routes requests to the right one, restarts them after failures, and puts them to sleep when idle. There is no instance list to babysit — objects exist because things (rooms, documents, users) exist.
What happens if my object gets too busy?
A single object is single-threaded, so one very hot thing has a throughput ceiling — that's the trade that buys you race-freedom. The documented scale-out pattern is more objects: shard a busy room into sub-rooms, a global rate limit into per-region buckets with a global reconciler, a hot queue into one object per worker shard. Each shard stays simple; the platform handles the fleet.
How is this different from Workers KV, D1, or R2?
All four store data; they differ in shape. KV is an eventually-consistent global key-value cache — great for config, wrong for coordination. D1 is one conventional Postgres-style relational database for queries across all your data. R2 is object storage for big blobs. A Durable Object is the only one that combines compute with strongly-consistent, per-entity state — the thing you reach for when “two requests must agree on one version of the truth.”
What happens to in-memory state when the object sleeps or moves?
In-memory variables are a cache, not a promise: when the object hibernates, migrates, or restarts, RAM resets and the constructor runs again on the next event. Anything that must survive goes through this.ctx.storage — the embedded SQLite — or per-socket attachments for WebSocket state. The standard pattern: load lazily on first use, write-through on change.
Can I really run this on the free plan?
Yes. Durable Objects are available on Workers Free with SQLite-backed classes: 100,000 requests and 13,000 GB-s of duration per day, plus 5 GB of storage. That's thousands of quiet chat rooms or a full hobby multiplayer game. The key-value storage backend is the legacy option, available only on paid plans for accounts that already used it.
Which languages can I use?
JavaScript and TypeScript are first-class. Python is supported in beta — the same class model, storage API, and deploy path. Everything on this page (bindings, migrations, hibernation) works from either.
Do clients connect directly to objects?
Objects aren't directly addressable from the public internet. A Worker sits in front: it authenticates, derives the object ID from something meaningful in the request (a room slug, a tenant host, a user ID), and forwards via a stub — or calls methods over type-safe RPC. That indirection is a feature: routing and authorization live in ordinary code you control.
What if the machine holding my object dies?
The runtime notices and starts a new instance of the object elsewhere — near users, per its placement — restoring from the replicated storage backend. Clients reconnect through the edge (or never notice, if they're on hibernating WebSockets held by the network). You lose at most in-flight, un-persisted memory — the same discipline as any actor model: persist what matters, treat RAM as a cache.
Build real-time apps your users can feel.
One class, one deploy, one less distributed-systems degree to earn. Start on the free plan — your first Durable Object can be live before your coffee cools.