Durable Objects unbundled — what happens when a bucket becomes the durability layer, the coordination service, and the control plane all at once
• celld gives a named entity — an agent, a workflow, a chat room — a single owning node, its own SQLite database, and durability via LTX replication to object storage.
• Object storage does more than back up state: conditional writes and epoch fencing tokens make it the coordination layer that decides who owns what.
• Each cell has its own transaction boundary. Global queries need a separate view, and adding nodes does not increase the capacity of an individual cell.
• For agents and workflows, I think the useful question is what should own the state. celld makes the named entity the unit of ownership and execution.
For most of the last decade, cloud application architecture has pushed us towards the same basic model:
Stateless application processes
│
├─────────┐
▼ ▼
Database Cache
│
QueuesApplication servers are disposable, and state lives somewhere else.
If two instances need to work on the same logical entity, they coordinate through databases, queues, locks, leases, optimistic concurrency or some combination of them.
An alternative is to change where the computation runs:
What if we moved the computation to the state instead?
That is the idea I found most interesting while reading about celld, a new open-source project from Deno.
celld is an open-source implementation of the Cloudflare Workers and Durable Objects programming model, built to run on infrastructure you own. It combines lightweight V8 isolates, SQLite, LTX replication and object storage to create something that looks like a persistent, movable actor.
The project is very early. I am looking at the architecture here, rather than recommending it for production.
One object, one owner
Imagine an application containing an agent:
agent-123In a conventional architecture, its state might be scattered across shared infrastructure:
API server
│
├── PostgreSQL
├── Redis
├── Kafka
└── object storageIf multiple workers interact with the same agent, they need to coordinate updates to its state. Another approach is to give agent-123 a single logical owner:
┌─────────────────────┐
│ agent-123 │
│ │
│ code │
│ memory │
│ SQLite │
│ timers │
│ connections │
└─────────────────────┘Requests concerning that agent are routed to the runtime currently responsible for it.
Instead of asking:
How do multiple workers safely coordinate over this state?
we ask:
Where is the owner of this state?
The runtime now has to locate that owner and route requests to it.
Compute becomes part of the stateful entity
celld uses lightweight V8 isolates rather than giving every logical entity its own container or operating-system process.
Conceptually, one machine can look like this:
celld node
│
├── cell: customer-42
├── cell: workflow-103
├── cell: agent-981
└── cell: chat-room-17Each cell can run application code while owning its own persistent SQLite database.
The cell groups these resources together:
logical entity
│
├── compute
├── memory
└── databaseFor a warm cell, database access can be a local SQLite operation. The application does not need to cross a network boundary to execute a query, although making a write durable still involves replication.
Instead of:
application
│
network
│
▼
PostgreSQLwe get:
V8 isolate
│
▼
SQLiteCloudflare Durable Objects use the same architectural idea: colocate the state and the computation responsible for it.
But where does durability come from?
Local SQLite access is useful, but we still need to recover the database if the VM disappears.
celld combines SQLite with LTX, the transaction format used by the Litestream ecosystem.
The rough model is:
Cell
│
▼
SQLite
│
▼
LTX transaction
│
▼
Object storageThe local SQLite database acts as the hot operational store.
Changes are captured as LTX and copied off the owning node before celld tells the caller the write succeeded. In the default fleet mode, it can report success once the required copies are saved on other nodes, or once the upload to object storage finishes, whichever happens first. If the other nodes confirm first, the caller does not have to wait for the bucket upload.
With no replication peers, every write waits for object storage. The team can also select bucket mode to require that wait even when other nodes are available.
SQLite transaction
│
▼
changes captured as LTX
│
▼
required copies saved on other nodes
or upload to object storage complete
│
▼
caller receives successIn celld v0.5.1, this durability wait is always enforced. The documentation describes a recovery point objective of zero against a single-node failure: losing the machine holding the local SQLite database should not lose a write already reported as successful.
Until the bucket upload finishes, recovery depends on the copies held by the nodes. If all those copies are lost before the upload completes, the write can be lost too. The third post explains the write and recovery paths in detail.
Object storage becomes more than backup
The part I found most interesting is how celld uses object storage to decide who owns a cell.
Suppose we have three celld nodes:
Node A
Node B
Node COne of them currently owns:
agent-123Somewhere the system needs an authoritative answer to:
Who owns agent-123?Distributed systems traditionally introduce infrastructure such as etcd, Consul, ZooKeeper, or Raft-based metadata services to answer that question.
celld instead makes use of conditional object-store operations.
Conceptually, object storage might contain something equivalent to:
cell: agent-123
owner: node-B
epoch: 94Node B can process the agent. If it disappears, another node can eventually obtain ownership:
Node B disappears
│
▼
lease expires
│
▼
Node C claims ownership
│
▼
epoch becomes 95Node C can restore the SQLite state from the durable transaction history and restart the cell.
The epoch acts as a fencing token.
If Node B unexpectedly comes back and still believes it owns the cell, its older epoch can prevent it from continuing to make authoritative writes.
The system relies on the object store’s consistency guarantees for this decision. The provider operates the infrastructure behind those guarantees.
Follow one request
The design becomes easier to understand if we follow a single request.
Suppose an HTTP request arrives for:
agent-123It reaches Node A, which determines where agent-123 currently lives. There are three cases.
Case 1: Node A already owns the cell
The request can execute locally:
request
│
▼
Node A
│
▼
agent-123
│
▼
SQLiteNo forwarding to another node is needed.
Case 2: another node owns the cell
If Node B owns the cell, Node A forwards the request:
client
│
▼
Node A
│
▼
Node B
│
▼
agent-123Node A routes the work to Node B, which processes it against the cell’s database.
Case 3: nobody currently owns it
Perhaps the previous owner has failed or released ownership.
A node can claim ownership, restore its database and restart its runtime:
claim cell
│
▼
restore SQLite
│
▼
start isolate
│
▼
process requestA cell can retain its identity and durable state while its runtime is unloaded.
Hibernation changes the economics
Imagine a system containing one million agents, with only 2,000 active at a time. Keeping a container running for every agent would allocate resources to a mostly idle population. Hibernation lets us size the compute fleet around the active working set.
Inactive cells can exist primarily as durable state:
object storage
agent-1 ────────────────┐
agent-2 ────────────────┤
agent-3 ────────────────┤
│
... │
│
agent-999999 ────────────┘while only the active working set consumes significant compute resources:
Node A
├── agent-412
├── agent-872
└── workflow-192
Node B
├── agent-982
└── customer-771When activity arrives, the cell wakes. When activity stops, it can hibernate again. Compute capacity depends on how many entities are active and how much work each one does; storage still has to accommodate the full population.
This looks a lot like the actor model
There is plenty of prior art for this model:
- Microsoft Orleans
- Akka
- Dapr Actors
- Cloudflare Durable Objects
A cell can be understood roughly as a persistent virtual actor.
The actor owns its state, and other components communicate with it through messages.
message
│
▼
actor
│
▼
stateThat gives us an explicit boundary for state access. In celld, the SQLite database sits within that boundary:
Cell
│
├── V8 runtime
│
└── SQLite
│
▼
LTX
│
▼
object storageEach actor has its own database.
Applying this to AI agents
This fits the agent workloads I am interested in: work continues across requests, and the runtime needs to retain state between them.
Even a relatively simple production agent quickly accumulates things such as conversation history, tasks, tool calls, tool results, memory, checkpoints, approvals, timers, events, and connections.
The API might expose a simple request handler:
POST /agent/runbut the runtime still has to manage that state.
We then reconstruct the agent from shared infrastructure on every iteration:
worker
│
├── load conversation
├── load memory
├── load workflow state
├── obtain locks
├── execute model/tool
├── persist results
└── release locksA stateful cell suggests a different model:
agent-123
│
┌───────┴────────┐
│ │
runtime SQLite
│
├── memory
├── messages
├── tasks
├── tool calls
└── checkpointsThen external events are routed to that agent:
user message ────┐
│
tool result ─────┼──> agent-123
│
timer ───────────┤
│
approval ────────┘The agent becomes the serialization boundary.
Rather than different workers fighting over the same state, events concerning an agent are processed by the runtime that owns it.
The application still needs to manage tool failures and retries, but it has a defined place to process events and update the agent’s state.
The same ownership model can be useful for long-running workflows. A WorkflowInstance has an id, state, current step, pending actions, timers and history. Those are responsibilities we also encounter in the Enterprise Integration Patterns Process Manager.
A stateful cell gives that workflow somewhere to run. Events for workflow-712 are routed to its owner, which advances the workflow and records the result. We still need the workflow logic, but the runtime handles locating the instance and activating it.
But there is a major trade-off: global queries
If all agent state lives in PostgreSQL, we can query across agents directly:
SELECT *
FROM agents
WHERE status = 'waiting_for_human'
ORDER BY created_at;With 100,000 agents owning 100,000 SQLite databases, there is no single database against which we can execute that query. We need another way to build the global view.
A state-per-cell design naturally pushes us towards something resembling CQRS:
┌── agent-1.sqlite
│
commands ──> cells ──┼── agent-2.sqlite
│
└── agent-N.sqlite
│
│ events
▼
shared projections
PostgreSQL
ClickHouse
Kafka
search index
analyticsThe cell database holds authoritative transactional state, and shared projections provide global views.
For example, an agent could emit:
AgentWaitingForApprovaland a PostgreSQL projection could maintain:
agents_waiting_for_approvalThis gives us:
Local consistency
+
Global eventual consistencyIf the application needs many queries across entities, maintaining these projections could outweigh the benefits of local state ownership.
Per-cell throughput and cross-cell transactions
The query problem isn’t the only cost of this model.
A cell runs on a single thread, owned by one node at a time. Adding nodes gives the fleet room for more cells, but does not give an individual cell more execution capacity. A busy chat room or an agent receiving many tool results may reach that limit.
The other cost is cross-cell transactions. If a workflow needs to atomically update two agents — or an agent and a workflow — at once, there’s no shared transaction to reach for. Each cell is only consistent with itself. Coordinating across cells means falling back to sagas, compensating actions, or eventual consistency between them: the same techniques distributed systems have always used when a single ACID transaction isn’t available.
I would check both requirements early. If most requests operate on one entity, the boundary may fit well. If they routinely update several entities together, the application has more coordination work to do.
This is not self-hosted Cloudflare
Cloudflare Durable Objects come with a managed platform covering routing, TLS, DDoS protection, deployment, networking and regional infrastructure. With a self-hosted runtime, the team must provide and operate the surrounding services.
So I would distinguish between:
Durable Objects
programming model
+
runtime
+
global infrastructure
+
managed operationsand something like:
celld
programming model
+
runtimeThat operational work belongs in any comparison with the managed platform, alongside the maturity of celld itself.
The interesting part is the architecture
The design combines established ideas: actors, SQLite, object storage, fencing and V8 isolates. What I find useful is how they fit around a named entity:
named entity
│
▼
lightweight runtime
│
┌────────┴────────┐
│ │
compute SQLite
│
▼
LTX
│
▼
object storageFor an application whose work fits within individual cells, the core deployment could look like:
load balancer
+
stateful cells
+
object storageGlobal projections, ingress and other application requirements still sit around that core. It gives us a useful starting point for deciding which services we actually need.
What should own the state?
The question I would take into a design discussion is:
What is the correct unit of state ownership in a distributed application?
For a traditional CRUD application, a central relational database may still be exactly the right answer.
But for systems dominated by long-lived autonomous entities — agents, workflows, games, collaborative rooms, devices, sessions — the answer may increasingly be:
the entity itselfThe runtime becomes responsible for locating the entity, activating it, moving it and keeping its state durable.
We normally move data to compute:
worker starts
│
▼
load state
│
▼
compute
│
▼
save stateArchitectures such as Durable Objects and celld move us closer to:
event arrives
│
▼
find state owner
│
▼
send computation thereFor agents and long-running workflows, I think that is worth evaluating. Start with the state boundary, then check whether the workload can live within it: how much work reaches one entity, which operations cross entities, and which queries need a global view.
The rest of this series
This post is the architectural overview. Three follow-ups take apart the mechanisms it sketches:
- How celld Turns Object Storage Into a Coordination Service — the four bucket properties celld needs, compare-and-swap ownership, and epoch fencing
- How celld Makes SQLite Writes Durable — the LTX write path in full, and why the bucket usually isn’t what you wait for
- How celld Coordinates a Fleet Without a Separate Control Plane — node discovery, lease expiry and placement through the bucket, and the permissions a fleet credential grants
Related posts
- Why Agents Make Good Actors — why the actor model fits the shape of AI agent workloads
- Agents, Routing, Patterns, and Actors — EIP patterns and actor runtimes for multi-agent coordination
- Agents Are Still Just Software — the distributed systems frame for agent architecture
Loading comments...