Durable Objects for Per-Entity State in Cloudflare Workers: An Architecture Note
Use Durable Objects when a single entity needs serialized writes and read-after-write consistency. This note covers trust boundaries, single-region data homing, operational checks, and when to choose KV, R2, or D1 instead.
02 Sept 2025, 20:23 UTC

The problem: shared mutable state at the edge
When multiple requests must mutate the same entity—a counter, a chat room, a game lobby—a stateless Worker paired with Workers KV or R2 gives you eventual consistency and lost updates. Durable Objects address this by giving each entity a single, addressable instance with transactional storage and serialized execution.
Smallest suitable design
The minimal architecture is one Worker that receives traffic, performs authorization, and forwards the request to a Durable Object bound by name. The object owns the entity state and executes writes one at a time.
Example wrangler.toml (run in your project directory; deploy requires Workers Admin or equivalent permissions):
name = "entity-worker"
main = "src/index.js"
compatibility_date = "2025-01-01"
[[durable_objects.bindings]]
name = "ENTITY"
class_name = "EntityObject"
[[migrations]]
tag = "v1"
new_classes = ["EntityObject"]
A minimal Worker and Durable Object class:
export class EntityObject {
constructor(state, env) {
this.state = state;
this.env = env;
}
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === "/increment") {
const current = (await this.state.storage.sql.exec(
"SELECT value FROM counters WHERE id = 1"
).one())?.value ?? 0;
const next = current + 1;
await this.state.storage.sql.exec(
"INSERT OR REPLACE INTO counters (id, value) VALUES (1, ?)",
next
);
return new Response(String(next));
}
return new Response("Not found", { status: 404 });
}
}
export default {
async fetch(request, env) {
// Trust boundary: authorize here before touching object state.
if (request.headers.get("X-Entity-Token") !== env.ENTITY_TOKEN) {
return new Response("Unauthorized", { status: 401 });
}
const id = env.ENTITY.idFromName("shared-entity");
const stub = env.ENTITY.get(id);
return stub.fetch(request);
}
};
This assumes a Workers runtime with SQLite-backed Durable Objects. Storage backend and alarm semantics have evolved across versions, so confirm your account and Wrangler version support before relying on storage.sql.
Trust and data boundaries
The Worker is untrusted edge code. Durable Object storage is isolated per object and is not directly reachable except through the Worker’s fetch or RPC. Authorization must be enforced in the Worker before forwarding; the object should treat inbound requests as pre-authorized only if the Worker is the sole caller.
Object IDs are deterministic but not secret. Do not use them as capability tokens without additional authentication.
State lives in a single homed region. Cross-region access adds latency, so global designs should shard by entity ID rather than expecting low-latency worldwide writes to the same object.
Operational checks
- Monitor alarm scheduling, storage size, and WebSocket hibernation metrics. Alarms and hibernatable WebSockets reduce cost but require idempotent handlers because wake-ups are not guaranteed to be immediate.
- Use
wrangler tail(run from the project directory; requires Logs Viewer permission) to inspect object initialization, alarm, and hibernation events under load. - Compare latency and error rates when accessing the object from different regions to validate single-homed behavior for your traffic pattern.
Failure modes
A single object is a single point of failure for that entity. If its region is impaired, requests to that object may fail or stall while other objects remain available. Design retries with exponential backoff and consider multi-object redundancy for critical entities.
Long-running compute should be offloaded to Queues or other Workers because Durable Objects have CPU and memory limits per object. High-frequency writes to a single object can also become costly under duration-based pricing.
When to change the design
| Requirement | Better fit |
|---|---|
| Read-heavy with acceptable staleness | Workers KV or R2 with cache |
| Relational queries across many entities | D1 or an external database |
| Global low-latency writes to the same entity | A consensus system outside Workers |
Practical verification
- Deploy the minimal Worker with
wrangler deploy. - Send sequential
/incrementrequests from multiple clients and confirm the counter increases monotonically without lost updates. - Inspect
wrangler tailoutput for object initialization and any alarm events. - Repeat the requests from different regions and compare latency to confirm the object remains homed.
Limitations and rollback
Durable Objects are not a relational store across entities and are not a globally consistent write layer for a single hot entity. Verify current limits, SQLite support, and pricing in your Workers runtime version before production use.
If you need to remove this design, redeploy the Worker without the Durable Object binding. Existing object state persists until explicitly deleted through the dashboard or supported tooling; deleting state is irreversible.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.