Cloudflare / 03
Durable Objects,
from the inside.
Some state has to be agreed on: the last seat, the next move, the current revision. A Durable Object gives each such thing one coordinator with its own storage, found by name. Run one.
Illustration One room, several clients
-
One room. Tabs that join
room:heronreach one object, chosen by the Worker withROOMS.getByName("heron"); a tab that moves toroom:foxreaches a different object. A turn saved to SQLite survives hibernation: the next request runs the constructor and reloads it. A turn kept only in working memory is gone. Connections stay open through hibernation only when the object accepted them withctx.acceptWebSocket(); an object holding standard sockets does not hibernate, and a shutdown terminates them. - The last seat. Two Workers that each read one open seat and then write zero accept two players for one seat. One object that checks and reserves without yielding accepts one and answers “room full” to the other.
- Several rooms. Six messages for three rooms take two rounds with one object per room, six with one object for everything. A round is a turn in this model, not a measurement.
- Request, message or update
- Storage call
- Memory ahead of SQLite: not saved yet
- Dashed object: hibernated, only its saved state remains
- Dotted line: a socket held through hibernation
Illustration Stop at the await
Two calls to increment() run in one object. If A reads
the counter, awaits an external service, and B runs and writes
during that wait, A writes back its old value plus one: one change
is lost. When the handler awaits first and then updates with one SQL
statement, the same order keeps both changes.
An external await is not a lock: while A waits, another event can run
in the object. Input and output gates protect storage operations, not
an outside fetch(). Writes with no await between them
commit together; transactionSync draws an explicit
commit-or-rollback boundary, as the example does for its revision and
command receipt.
The runnable example: three files
A small Worker and object that persist a revision, reject a stale
move, and remember a command ID so a retry does not apply it twice.
Save the files in a new folder and follow try.sh. The
Worker accepts only local requests, and this page never runs it.
Before public use it needs identity, per-room permissions, limits,
and a retention policy for receipts. When you change a deployed
class, add migrations after room-v1 and never edit an
applied one.
worker.js · the Worker and the object
Download ↓
wrangler.jsonc · the binding and the migration
Download ↓
try.sh · the local commands
Download ↓
Read the mechanism
One name leads to one object
A class defines the behavior; a namespace binding names the
collection of objects of that class. getByName()
returns a stub, and the same name in the same namespace reaches the
same object wherever the caller is. Cloudflare creates an object
near the first request for it, and today an object does not move
after that. A location hint is best effort; a jurisdiction is
strict.
The code can stop; the saved state stays
Each SQLite-backed object has its own private database through
ctx.storage.sql, recommended for new classes. When an
object hibernates or is evicted, its working memory is discarded,
and the next event runs the constructor again. There is no shutdown
hook, so save when the state changes.
Connections and alarms
Sockets accepted with ctx.acceptWebSocket() stay
connected while the object hibernates; with ws.accept()
the object stays in memory, and a shutdown such as a deploy
terminates them. An object has at most one alarm at a time; its
handler runs at least once and is retried when it throws, so make it
safe to repeat.
Scale across rooms, not copies of one room
One object handles its events one at a time. A single object for
everything becomes a bottleneck; one object per room, document or
user lets unrelated work proceed separately. Separate objects’
databases never become one transaction.
blockConcurrencyWhile is for initialization and
migrations, not for holding requests around slow outside work.
Sources and limits
Cloudflare pages used
- What are Durable Objects? : named objects, private storage, single-threaded execution.
-
Stubs and remote methods : namespace bindings,
getByName(), and RPC. - Data location : placement near the first request, no relocation today.
- Object lifecycle and in-memory state : discarded memory, a rerun constructor, no shutdown hook, no hibernation with standard sockets, sockets terminated on shutdown.
-
SQLite storage API : synchronous
sql.exec, atomic writes without an await, andtransactionSync. - Durable Objects: Easy, Fast, Correct : input and output gates.
-
Rules of Durable Objects : one object per logical unit, and
blockConcurrencyWhile. - WebSocket hibernation , alarms and getting started .
Documented and illustrated
- Documented: identity by name, the reloaded saved state and the lost working memory, sockets kept only with the Hibernation API, alarms, the lost update at an external await, and the atomic update.
-
Illustration: the tabs and their names, the two
locations, rooms
heron,foxandowl, revisions 7 and 0, the seat, the counter, the burst of six messages, and the rounds. The scheduling of A and B is yours. - Runnable: the example files are real code, tested in a local Workers runtime with temporary storage.
Left out on purpose
- Pricing, latency, throughput, storage size, and request limits, including the per-object request figure, a soft limit.
- How long an idle object stays in memory, which Cloudflare does not promise. In the instrument it hibernates when you say so.
- The key-value storage backend, point-in-time recovery, and how many times an alarm is retried.