15 minutes · No hands-on code · Runtime mechanics
Listen while you read — AI-generated podcast for this lesson
Lesson 1 established why a DO exists. This lesson is about what the runtime actually does on your behalf. The goal: after this lesson you could explain the mechanics to someone else without notes.
A Durable Object has five runtime properties that work together. None of them are magic once you can name them. And none of them are unique to Cloudflare — the same properties appear in Microsoft Orleans (“grains”, used in Azure, Halo, Xbox), Erlang processes, and Rivet Actors (a newer platform that runs on Node.js and is self-hostable). Cloudflare’s implementation is distinctive because of the global edge routing and co-located SQLite; the underlying model is a 50-year-old distributed systems pattern called the Actor model. Understanding these five properties means you understand the pattern, not just Cloudflare’s version of it.
| Property | What the runtime does |
|---|---|
| Global uniqueness | Routes all requests for a given ID to exactly one instance, anywhere in the world |
| Single-threaded execution | Serializes request handling inside that instance — no two handlers run concurrently |
| Co-located storage | Gives each instance a private SQLite database on the same machine as the instance |
| Input & output gates | Prevents new events from arriving and new responses from leaving while storage writes are in flight |
| Hibernation | Evicts idle instances from memory while keeping WebSocket connections alive |
Each one earns its place. Let’s go through them.
When a Worker asks for a DO by name — say env.ROOMS.get(id) — the
runtime resolves that name to a specific Cloudflare data center and routes the request there.
The same name always resolves to the same location. Any Worker, in any data center in the
world, asking for the same name gets routed to the same single instance.
The routing is deterministic and handled entirely by the runtime. Your code never decides where an instance lives — you just use the name. The runtime handles the rest.
IDs vs. names. You can address a DO by a random ID
(env.ROOMS.newUniqueId()) or by a name you choose
(env.ROOMS.idFromName("room-42")). Both resolve to a globally unique instance.
Name-based IDs are deterministic: the same name always produces the same ID, which always
routes to the same instance.
Inside a DO instance, JavaScript runs single-threaded — the same as in a browser. Only
one thing runs at a time. But JavaScript is async, which means a handler can await
something and yield, allowing another handler to start running.
This is where the race condition from Lesson 1 would re-enter. Two concurrent handlers could both read a value, both modify it, both write — and you’re back to the same problem.
The runtime prevents this with input gates.
These are the runtime mechanism that makes single-threaded async code actually correct. Kenton Varda coined the terms in the 2021 design post.
Input gate: While a storage operation is in flight (during an
await), the runtime holds back any new incoming events — requests, responses
to outbound fetches, anything. The gate opens only when the storage operation completes and
the current JavaScript turn finishes. This means your read-modify-write sequence cannot be
interrupted by another request sneaking in between the read and the write.
Output gate: While a storage write is in flight, the runtime holds back any
outgoing network messages — responses to the client, outbound fetch() calls.
If the write fails, the held-back messages are dropped and the DO restarts. This means you can
fire a storage.put() without awaiting it and still be safe: the
runtime will not let a “success” response reach the client before the write lands on
disk.
Why this matters for teaching. When someone says a DO “automatically handles concurrency”, this is the specific mechanism they mean. It is not magic. It is two runtime-enforced gates that prevent events from entering or responses from leaving during a storage operation. You can now name the mechanism.
There is one important caveat. If you deliberately start two async operations from the same synchronous turn —
// Both calls start before either awaits — gates won't help here
const p1 = this.getUniqueNumber();
const p2 = this.getUniqueNumber();
await Promise.all([p1, p2]);
— the gates cannot protect you, because there is no incoming event to block. You initiated both calls yourself in the same turn. This is a deterministic bug and easy to catch in testing, which is why the runtime treats it as the developer’s responsibility.
Each DO instance has a private SQLite database that lives on the same machine as the instance. Reads and writes do not leave the machine. This is why storage feels fast compared to a traditional database: there is no network hop.
The storage is private to one instance. No other DO can read it. No Worker can access it directly. The only way in is through the DO’s own methods.
Writes are also cached in memory automatically. A storage.put() writes to the
in-memory cache immediately and returns. The cache is flushed to disk asynchronously. The
output gate ensures the client never sees a confirmation before the flush succeeds. So from
your code’s perspective, writes are instant; from the client’s perspective, they
are durable before any response arrives.
A DO instance starts cold — the constructor runs on the first request. It stays alive in memory as long as requests keep arriving. When it goes idle, one of two things happens depending on whether it has open WebSocket connections:
The key word is hibernateable. A DO can only hibernate if there are no pending timers, no in-progress awaited fetches, no active outbound connections, and — crucially — no WebSockets opened with the standard WebSocket API. If you use the Hibernation WebSocket API instead, the runtime can hibernate the instance while keeping client connections alive and wake it up only when a message arrives.
The implication for your code: in-memory state does not survive
hibernation. Anything you need across requests must be in storage. Anything you keep in
this.someVar is a cache, not a source of truth.
Here is how the five properties interact in a single request:
That is the full runtime contract. No other mechanism is involved in the coordination story.
Select the best answer. Commit to one before revealing.
A DO handler awaits a storage.get(). A second request arrives
at that exact moment. What does the runtime do with it?
A DO calls storage.put("key", value) without awaiting it, then
immediately returns a 200 response. What actually happens?
A DO has 50 WebSocket clients connected using the standard WebSocket API. It goes idle. What happens after 10 seconds?
A DO stores this.count = 42 in a handler. The DO hibernates. A new request
arrives. What is this.count?