chaowen. · Notes

Distributed systems / cloud architecture · 2026-10-16

Durable Objects: the distributed singleton that makes state explicit

Cloudflare's October 9 announcement that Deno is joining Cloudflare is a timely excuse to inspect a deeper idea behind the Workers ecosystem: Durable Objects. The name can sound like a storage product. The useful mental model is different: a Durable Object is an addressable, stateful actor, and the platform routes work for one object identity to a single active execution context at a time. That lets application code coordinate updates without making every request race through a shared database row.

The problem: shared state turns requests into competitors

Imagine a chat room. Many clients send messages concurrently, and the application must assign a stable order, broadcast events, and persist history. If every stateless request independently writes to a database, the application must build coordination: locks, transactions, retry logic, and an ordering rule. A process-local variable is not a solution either: two instances may each believe they own the room.

Instead, assign each room a stable object identity. All operations for room blue address the same logical object, wherever it currently runs. Its single-threaded event loop serializes state-changing operations; its durable storage keeps state across object eviction or restart. You get a clear owner for a small piece of state, not a magically global transaction.

client request ── room id: blue ──▶ object blue
                                      │
                         serialized event handling
                                      │
                         local durable state + sockets

room id: green ───────────────────▶ object green

Why this is more than “one server per key”

A conventional keyed service might hash an ID to a server, but then you own rebalancing, failover, and ensuring that two servers do not both act as the owner during a transition. A managed actor abstraction moves much of that placement and routing work into the platform. The abstraction is a distributed singleton: one logical coordinator per identity, while many identities can run across many machines.

That shape scales by partitioning the domain. One object per chat room, shopping cart, game match, or tenant means unrelated keys can progress independently. A single hot key still has a serialization ceiling: adding machines does not make one object's sequential critical section parallel. Partition choice is therefore a performance and consistency decision.

Durability is not the same as immortal memory

An object may be stopped and later instantiated again. In-memory fields are caches or transient state, not the durable record. Persist required state in the object's storage, then treat each invocation as a short-lived transaction boundary. WebSocket connections may be managed by the platform's object lifecycle, but client reconnect and application-level recovery remain design concerns.

Durable Objects can use SQLite-backed storage. That gives one object relational queries and transactional updates local to its identity. It does not imply that a transaction spanning two different objects is atomic. If a transfer changes account A and account B, you still need a cross-object protocol: for example, an idempotent saga with durable steps and compensating actions.

Small reproducible design exercise

Sketch a counter service where each named counter must never lose an increment. The pseudocode below illustrates the shape, not a copy-paste deployment API:

async function increment(counterId) {
  const object = getDurableObject(counterId);
  return object.runTransaction(async (tx) => {
    const old = (await tx.get("value")) ?? 0;
    const next = old + 1;
    await tx.put("value", next);
    return next;
  });
}

Run a local test with hundreds of concurrent increments for the same ID, then for several IDs. Assert that one counter ends at the total number of successful operations, and that independent counters do not share values. Restart the local process and confirm persisted values survive. Finally inject a client retry after the server commits but before the response arrives: this reveals that durable state alone does not make a non-idempotent request safe. Add a request ID and store processed IDs with the update.

Failure modes to design for

Takeaway

A distributed singleton is useful because it makes ownership and serialization explicit at a chosen partition key. It simplifies local coordination, while leaving important distributed-systems work—idempotency, cross-key workflows, hot-key limits, and recovery—squarely in view. Use it when a bounded piece of state benefits from one logical writer; do not mistake it for a universal database transaction.

Primary sources