Cloudflare Durable Objects Explained: Stateful Edge Architecture, Storage, Alarms and Limits
Serverless platforms made compute disposable, and that is exactly why shared state has always been awkward on them. Two stateless functions that must agree on a seat booking, a document edit, or a rate-limit counter have to go through a database, a lock service, or a queue, and each of those adds latency and a new failure mode. Cloudflare Durable Objects take the opposite approach: instead of moving state to a central store, they give every logical entity its own tiny, single-threaded server with a private database attached, addressable by name from anywhere on the network.
This matters in 2026 because the model has matured well beyond its original key-value form. SQLite-backed objects, 30-day point-in-time recovery, hibernating WebSockets, and per-object alarms now make Durable Objects a credible foundation for multi-tenant SaaS, collaborative editing, game servers, and AI agent sessions. They also have sharp edges: a hard serialization point, a per-object throughput ceiling, and a billing model that punishes naive WebSocket code.
You will leave with a precise mental model of how an object lives, runs, stores data and wakes itself up, plus capacity and cost arithmetic taken from Cloudflare’s published numbers, and a checklist for deciding when this is the right tool.
What this covers: the programming model, the lifecycle and consistency guarantees, SQLite storage and recovery, alarms, WebSockets and hibernation, placement, limits, pricing math, failure modes, and a decision framework against the alternatives.
Context and Background
Most edge platforms scale by being stateless. A request lands on any machine in any city, executes, and disappears. That design is superb for read-heavy, cacheable work and poor for anything that needs a single authoritative answer. If two users in Mumbai and Frankfurt both try to claim the last ticket, something must serialize them, and “something” is traditionally a database in one region with a connection pool in front of it.
Cloudflare Workers, the runtime underneath, already covers the stateless half well. Our comparison of serverless cold starts across Lambda SnapStart, Cloudflare Workers and Cloud Run shows why isolates start so quickly. What Workers lack by themselves is coordination. Cloudflare Durable Objects were introduced to fill that gap, and Cloudflare’s own documentation describes a Durable Object as “a special kind of Cloudflare Worker which uniquely combines compute with storage.”
The actor model, applied to infrastructure
The closest conceptual ancestor is the actor model: a unit of state and behaviour that processes one message at a time and talks to other actors only by messages. Erlang processes, Akka actors, and Microsoft Orleans grains are all in this family. A Durable Object is an actor whose address is a name or ID, whose mailbox is the network, and whose durable state is an embedded database. Unlike a self-hosted actor framework, you never provision nodes, rebalance shards, or write a placement service. The platform creates an object implicitly on first access and migrates it among healthy servers.
Where the alternatives sit
It helps to name the incumbents honestly. A regional Postgres gives you rich queries and transactions but concentrates latency and contention in one place. Redis gives you fast shared counters but you operate the cluster and reason about failover. DynamoDB conditional writes give you optimistic concurrency at scale but no place to run logic next to the data. A message queue gives you ordering per partition but not request-response semantics. Cloudflare Durable Objects are not strictly better than any of these. They are a different trade: strong consistency and colocated compute for each atom of coordination, in exchange for giving up cross-object transactions and ad hoc global queries.
Cloudflare’s Durable Objects documentation is the primary source for every number in this article. Where I derive a figure from those numbers, I say so, and where a value is illustrative I label it.
The Cloudflare Durable Objects Architecture: One Name, One Instance, One Database
A Durable Object is addressed by a globally unique name or ID. At any moment at most one instance of it runs anywhere in the world, it executes JavaScript or WebAssembly on a single thread, and it owns a private, strongly consistent SQLite database that lives on the same machine. Every request for that name is routed to that one instance.

Figure 1: Stateless Workers on any edge node resolve a name to one Durable Object instance, which owns its memory, SQLite database and alarm.
Figure 1 shows the shape. Requests arrive at ordinary Workers close to the user. A Worker holds a namespace binding to a Durable Object class, derives an ID (from a name, or a random unique ID), builds a stub, and calls a method on it. The platform forwards that call to wherever the single live instance of that object runs. The Worker never needs to know the location.
The programming model in a page
You declare a class that extends DurableObject, bind it in the Wrangler configuration, and call methods on stubs. Cloudflare recommends RPC methods over the older fetch() handler for object-to-Worker calls. A minimal SQLite-backed counter with an alarm looks like this.
import { DurableObject } from "cloudflare:workers";
export class RoomCounter extends DurableObject<Env> {
sql: SqlStorage;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
// Schema setup runs on every cold start of this object
this.sql.exec(`CREATE TABLE IF NOT EXISTS hits (
room TEXT PRIMARY KEY, n INTEGER NOT NULL DEFAULT 0)`);
}
async hit(room: string): Promise<number> {
this.sql.exec(
`INSERT INTO hits (room, n) VALUES (?, 1)
ON CONFLICT(room) DO UPDATE SET n = n + 1`, room);
const row = this.sql.exec(
"SELECT n FROM hits WHERE room = ?", room).one();
// Flush a summary one minute from now if nothing else is scheduled
if ((await this.ctx.storage.getAlarm()) === null) {
await this.ctx.storage.setAlarm(Date.now() + 60_000);
}
return row.n as number;
}
async alarm() {
// Idempotent flush to an external system would go here
}
}
export default {
async fetch(req: Request, env: Env): Promise<Response> {
const stub = env.ROOM_COUNTER.getByName("lobby-42");
return Response.json({ n: await stub.hit("lobby-42") });
},
};
// wrangler.jsonc
{
"name": "room-counter",
"main": "src/index.ts",
"compatibility_date": "2026-10-01",
"durable_objects": {
"bindings": [{ "name": "ROOM_COUNTER", "class_name": "RoomCounter" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["RoomCounter"] }]
}
Two details matter. First, the migration uses new_sqlite_classes; Cloudflare recommends SQLite-backed storage for all new classes, and on the Workers Free plan it is the only backend available. Second, getByName("lobby-42") is deterministic: the same string always resolves to the same object, which is how independent Workers rendezvous without any registry.
Why one instance makes correctness cheap
The guarantee that does the real work is global uniqueness. Because only one instance exists, there is no replica to reconcile, no quorum to assemble, and no lock to acquire. Two simultaneous “claim the last ticket” requests are both delivered to the same event loop, and the second sees the effect of the first. The runtime protects this property aggressively. During a shutdown, Cloudflare’s lifecycle documentation says that an in-flight request which attempts to access storage “will be stopped immediately and return an error to maintain Durable Objects global uniqueness property.” The platform prefers failing a request to risking two writers.
Input gates and output gates
JavaScript is cooperatively scheduled, so any await is a point where another request could interleave and break an invariant. Durable Objects close this hole with two mechanisms described in Cloudflare’s engineering writing and referenced in the storage API docs. An input gate holds back new events while the object is waiting on a storage operation, so a read-modify-write across an await cannot be interrupted by another request touching the same data. An output gate holds back outgoing network messages until pending writes are confirmed durable, so a client cannot observe a response that depends on data which is later lost.
The practical consequence is striking. You can write ordinary-looking await storage.get() then await storage.put() code and get serializable behaviour for storage, without explicit transactions in most cases. The caveat, documented in the rules page, is that gates only cover storage. If you interleave a non-storage await, such as an outbound fetch(), other requests can run in the gap, and you must reason about that race yourself.
Placement: close to the first caller, then fixed
An object is created in a data center near the location of the first get() call, and Cloudflare states that objects “do not currently change locations after they are created,” with dynamic relocation described as planned. You can pass a locationHint such as enam, weur, or apac on the first call; hints are best effort, and some regions (South America, Africa, Middle East) map to a nearby region that supports objects. For compliance you can instead create a jurisdiction-restricted namespace for eu, us, or fedramp, which constrains where the object runs and stores data. That is a hard guarantee, unlike a hint.
This fixed placement has a design implication that surprises people: a user in Singapore talking to an object created from Frankfurt pays the round trip every time. The cure is to create objects in response to real traffic, or hint deliberately, rather than pre-creating them in a batch job from one region.
Lifecycle, Storage, Alarms and WebSockets in Depth
With Cloudflare Durable Objects, understanding when an object is in memory, when it is billed, and when it silently forgets its variables is the difference between a cheap system and a surprising invoice. This section walks the lifecycle first, then the three capabilities that hang off it.
The lifecycle: five states and a ten-second timer
Cloudflare’s lifecycle documentation defines five states. An object starts inactive. The first request or event, such as an alarm, runs the constructor() and moves it to active, in-memory. When work finishes it becomes idle in memory, either hibernateable or non-hibernateable. A hibernateable idle object is removed from memory after 10 seconds of inactivity, whereas a non-hibernateable idle object is evicted entirely after 70 to 140 seconds with no requests or events.

Figure 2: Lifecycle states of a Durable Object. Duration is billed while active or idle-but-non-hibernateable, not while hibernated or inactive.
The hibernation criteria are strict, and each one is a trap. All four must hold: no setTimeout or setInterval callbacks pending, no unfinished I/O or waitUntil() promises and no open outbound connection, no use of the standard WebSocket API, and no request still being processed. A single forgotten setInterval heartbeat keeps the object in the billed state indefinitely. In-memory variables vanish on hibernation or eviction, and the constructor runs again, so anything important must live in storage.
Pending I/O has a ceiling worth knowing. Each operation that prevents eviction, such as an outbound fetch(), a service binding call, or an open outbound WebSocket, does so for up to 15 minutes from when it starts. Operations started later can extend residency, because the limit is per operation rather than a total.
Storage: SQLite embedded next to the code
Each SQLite-backed object has its own database, reached synchronously through ctx.storage.sql.exec(). Synchronous matters. Because the database is on the same machine as the code, a query returns without yielding the event loop, and Cloudflare’s state documentation notes that SQLite operations “do not yield the event loop, so they execute atomically” without blockConcurrencyWhile. A read-heavy handler therefore does not pay a network hop per query, and you can freely issue several small queries where a remote database would force you to batch.
The platform enforces specific limits. Storage per object is 10 GB on Workers Paid (5 GB total per account on Free, which the FAQ phrases as 1 GB per object). A row, string, or blob can be at most 8 MB, a table can have at most 100 columns, a single SQL statement is limited to 100 KB, and a query can bind at most 100 parameters. There is no cap on the number of objects or the total storage per account on the paid plan, which is the key to the per-tenant database pattern.
Transactions deserve a note. sql.exec() rejects BEGIN TRANSACTION and SAVEPOINT; you use ctx.storage.transaction() or transactionSync() instead. When storage fills, writes fail with SQLITE_FULL but reads and deletes continue, so a full object can always dig itself out by deleting data.
The key-value API still exists as a thin layer. Cloudflare’s pricing page confirms that get(), put(), delete() and list() on a SQLite-backed class “store and query data in a hidden SQLite table” and are billed as rows read and written. Legacy key-value-backed classes remain available only to accounts that already had a namespace, with smaller limits: 2 KiB keys and 128 KiB values.
Point-in-time recovery: a 30-day undo button
The storage API includes a recovery interface that is unusually practical. getCurrentBookmark() returns an opaque, lexically comparable string identifying a moment in the object’s history. getBookmarkForTime() returns a bookmark for approximately any time within the last 30 days. onNextSessionRestoreBookmark() arms a restore for the next restart, and the call returns a bookmark for the instant just before the restore, so the recovery itself can be undone. The documented pattern is to arm the restore and then call ctx.abort() to restart the object.
This turns a class of incidents, such as a buggy migration that corrupted one tenant’s data, into a per-object rewind rather than a restore of a whole database. The limit is that it applies to one object at a time, so a bad deploy affecting a million objects needs a fleet-wide script of your own.
Alarms: durable timers for each object
An alarm schedules the object’s alarm() handler to run at a future time, with millisecond precision. Three rules define how to use it. Each object can hold only one alarm at a time, and calling setAlarm() again overrides it. Execution is guaranteed at least once, with automatic retries using exponential backoff starting at 2 seconds and up to 6 retries when the handler throws. And alarms do not repeat; you must call setAlarm() again inside the handler.
At-least-once means handlers must be idempotent, which Cloudflare’s rules page states plainly. The standard design for many timers is to store a schedule table in SQLite and have a single alarm always point at the earliest due entry, as in the multi-event example in the official docs. An alarm handler may run for up to 15 minutes of wall time, and it counts as a request for billing, with each setAlarm() charged as one row written.
Alarms are what make an object more than a reactive endpoint. A rate limiter can expire windows, a booking object can release unpaid holds, and a game room can end a lobby after inactivity, all without any external scheduler and with the guarantee that the work happens even if no client ever calls again. Compare that with a Cron Trigger: a Worker can have only a handful of cron schedules, whereas every object can have its own alarm, so the fan-out is programmatic and per entity.
WebSockets and the Hibernation API
Long-lived connections are where Cloudflare Durable Objects shine and where bills go wrong. There are two ways to accept a socket. The standard approach calls ws.accept(), and the object stays in memory, billed for duration, for the entire time the socket is connected. The Hibernation API calls ctx.acceptWebSocket(ws) instead, and exposes webSocketMessage(), webSocketClose() and webSocketError() handlers. With it, the object can be evicted from memory while the clients remain connected, and the constructor re-runs when a message arrives.

Figure 3: A hibernated object wakes on an incoming WebSocket message, restores per-connection state from attachments and SQLite, then broadcasts and sleeps again.
Because memory is discarded, per-connection state must be rebuilt. serializeAttachment() stores a small structured-clone-able value on each socket that survives hibernation, and ctx.getWebSockets() returns the live sockets. For heavier state, such as chat history, use SQLite. For keepalives, setWebSocketAutoResponse() lets the runtime answer ping messages without waking the object, and Cloudflare notes these auto-responses incur no additional duration.
Two behaviours to plan for. First, if an object is shut down for a code update or by a placement decision, its WebSocket connections are terminated, and your client must reconnect and resynchronise. Hibernation keeps sockets alive across idle periods, not across restarts. Second, the maximum size of a received WebSocket message is 32 MiB.
Sharding by the atom of coordination
The cardinal design rule from Cloudflare’s guidance is to model objects around the smallest unit that needs to coordinate: a chat room, a document, a user, a tenant, a cart. A global singleton is explicitly named an anti-pattern, because one object funnels all traffic through a single thread. The healthy shape is a parent-child hierarchy, where a lightweight directory object lists children and each child handles its own traffic in parallel with its own database.
Deterministic IDs derived from names, such as getByName("doc:" + docId), give predictable routing with no lookup table. Random IDs from newUniqueId() suit cases where the caller does not choose the identity. Mixing the two, a name-addressed directory that hands out unique-ID children, is common in multiplayer matchmaking.
Cloudflare Durable Objects Limits and Cost: Doing the Arithmetic
Cloudflare Durable Objects reward back-of-the-envelope math more than most platforms, because every limit and price is per object and per operation. The figures below come from Cloudflare’s limits and pricing pages as they stood on the date of writing; always recheck before committing a budget, since pricing pages change.
The throughput ceiling of one object
Each object is single-threaded, and Cloudflare documents a soft limit of 1,000 requests per second per object. The rules page is more granular: roughly 1,000 requests per second for simple pass-through, about 500 to 750 for moderate JSON parsing and validation, and about 200 to 500 for heavier work involving transformation and storage writes. A request that arrives while the object is saturated is queued, and if the backlog grows the caller receives an overloaded error.
The way to read this is as a per-entity budget. A chat room with 200 members, each sending a message every 10 seconds, produces 20 inbound messages per second, which is two percent of the ceiling. A single popular live-stream chat with 100,000 viewers does not fit in one object, and you would shard it into many rooms with a fan-in tier. The ceiling is generous for coordination and brutal for aggregation, which is why a global rate limiter built on one object is called out as an anti-pattern.
Duration billing is the part people miss
The pricing page bills compute duration in wall-clock time while an object is actively running or idle but unable to hibernate, charged against a fixed 128 MB memory allocation regardless of real usage. The Paid plan includes 400,000 GB-seconds per month and then charges $12.50 per million GB-seconds. Requests are billed at $0.15 per million after 1 million included, and the page counts HTTP requests, RPC sessions, WebSocket messages, and alarm invocations. Incoming WebSocket messages are billed at a 20:1 ratio, so 100 messages count as 5 requests.
Here is a derivation of my own from those published numbers. One object kept permanently non-hibernateable for a 30-day month is active for 2,592,000 seconds. At 0.125 GB (128 MB) that is 324,000 GB-seconds, which sits inside the 400,000 included allowance but is the entire allowance for the account if you run just one. Run 100 such objects and you consume 32.4 million GB-seconds, roughly $400 at the overage rate. The documentation’s own Example 3 (100 objects each holding a WebSocket and receiving one message per second) lands at about $419 per month, which agrees.
The same workload with hibernation is dramatically cheaper. In the documentation’s Example 4, 100 objects each hold 100 hibernatable sockets receiving one message a minute, with 10 ms of handler time per message. The objects are executing JavaScript for roughly one second in each minute, and the page’s worked estimate comes to $20.65 per month in total, against $419.30 for the always-awake design in Example 3. The lesson generalises: bill is driven by time the object cannot sleep, not by connection count.
Storage billing: rows, not requests
SQLite-backed storage is billed in three dimensions on the Paid plan: rows read (first 25 billion per month included, then $0.001 per million), rows written (first 50 million included, then $1.00 per million), and stored data (5 GB-month included, then $0.20 per GB-month). Rates match D1, Cloudflare’s serverless SQL database. Deletes count as rows written, each setAlarm() counts as one row written, and the key-value methods map onto the same row counters.
The asymmetry is the whole story: writes cost a thousand times more than reads per row. Consider an illustrative counter object that performs one write per hit at a sustained 100 hits per second. That is 259.2 million rows written in a 30-day month; subtract the 50 million included and the overage is about $209. Batching ten increments into one write, or keeping the hot counter in memory and flushing it from an alarm every few seconds, cuts that by an order of magnitude. This is the main reason to combine alarms with in-memory state: coalesce writes, flush on a timer, and accept a few seconds of loss risk only where the product allows it.
An important consequence for index design: an index is extra rows written on every insert. Create indexes for columns you query frequently, as Cloudflare advises, but do not add speculative ones on a write-heavy table.
Other limits that bite
CPU time defaults to 30 seconds per invocation, with each incoming HTTP request or WebSocket message resetting the clock, and it is configurable up to 5 minutes through limits.cpu_ms. CPU time counts active processing, not waiting for I/O. Simultaneous outgoing connections per request are capped at 6, as in Workers. Outbound HTTP request bodies from an object are limited to 100 MiB by default. An alarm handler may run up to 15 minutes of wall time.
The configuration limits matter at scale too: 500 Durable Object classes per account on Paid and 100 on Free. Cloudflare points out that deploying a new Worker version completes immediately, but a code update strategy governs when each already-active object adopts the new code, with a deferred strategy giving objects time to hibernate first, bounded by max_delay.
Deeper Analysis: Patterns That Work and a Decision Matrix
The Cloudflare Durable Objects architecture is easiest to appreciate through the patterns it makes simple. Each below maps one entity to one object.
Pattern 1: collaborative document or whiteboard
One object per document. Clients connect with hibernatable WebSockets, send operations, and the object orders them, applies them to SQLite, and broadcasts. Ordering is free because the event loop is the sequencer. Presence data stays in socket attachments because it does not need to outlive the connection. Alarms compact the operation log after a quiet period. The object boundary also gives you natural access control: the Worker authorises a user for a document ID before it ever forwards a socket.
Pattern 2: per-tenant database
One object per tenant, each with its own schema and rows. Because the account has no cap on object count or total paid-plan storage and each object holds up to 10 GB, a SaaS can give every customer an isolated database co-located with the compute that serves it. Isolation limits blast radius: a runaway query slows one tenant’s object, not the fleet. The cost is that cross-tenant analytics need an export pipeline into a warehouse, since there is no global query. This is the pattern most worth considering if your current setup is a multi-tenant Postgres with noisy-neighbour problems.
Pattern 3: agent session
An AI agent conversation needs a durable transcript, tool results, and a scheduler for follow-ups. One object per session holds the message history in SQLite, keeps the model-streaming WebSocket, and uses an alarm to resume after a long tool call. Long-running streaming is workable because incoming HTTP and RPC calls have no hard wall-time limit while the caller stays connected. The same shape appears for IoT device twins: one object per device or asset keeps the latest state, receives telemetry, and fires alarms for heartbeat timeouts, which is the model used in our WebAssembly component model and wasmCloud edge architecture discussion of placing logic near devices.
Pattern 4: coordination primitives
Locks, leases, rate limiters keyed by tenant, and sequence generators all fit, provided the key space is sharded. A per-user limiter is a perfect object. A global limiter is the textbook anti-pattern.

Figure 4: Parent-child sharding. A directory object lists children; each child handles its own traffic, storage and alarm in parallel.
Decision matrix
| Need | Durable Objects | Regional SQL database | Redis or similar | Stateless Workers plus KV |
|---|---|---|---|---|
| Strong consistency per entity | Yes, by construction | Yes | Yes, single node | No, eventually consistent |
| Cross-entity transactions | No | Yes | Limited | No |
| Logic colocated with data | Yes | No | No | No |
| Global ad hoc queries | No, needs export | Yes | Limited | No |
| Per-entity timers | Yes, alarms | External scheduler | Expiry only | Cron only |
| Ops burden | None | Moderate | Moderate | None |
| Latency to users far from the object | One round trip | One round trip | One round trip | Low for reads |
The right reading is not a ranking. Choose Cloudflare Durable Objects when your data has a natural partition key and operations on one key must be serialized. Choose a relational database when your queries span entities. Many production systems use both, with objects as the real-time front and a warehouse behind it.
Relation to real-time edge systems
Teams building on Linux gateways will recognise the single-writer discipline from real-time design. Our tutorial on Linux 7.1 PREEMPT_RT, Zephyr and Xenomai for edge gateways argues for deterministic scheduling at the device. A Durable Object is the cloud-side counterpart: a deterministic, serialized owner for each device’s state, not a hard real-time system. Its latency is bounded by network distance and the single event loop, with no worst-case guarantee, so never put a safety loop in one.
Trade-offs, Gotchas, and What Goes Wrong
The Cloudflare Durable Objects model is powerful because it is restrictive. Every restriction below is the other face of a guarantee.
A single object is a single point of serialization. The same property that gives you correctness caps you at hundreds to a thousand requests per second per object. When the cap is hit, requests queue and eventually fail with an overloaded error. If your partition key is skewed, one celebrity tenant or one viral room becomes the bottleneck, and the platform will not split it for you. You must design sub-sharding, for example by hashing a room into N shards and aggregating, before the hot key appears.
Placement is sticky. Objects do not move after creation. An object born from a request in the wrong region serves every later request from there. Pre-creating objects from a batch job in one region is the classic way to give distant users permanent latency. Use real traffic, or explicit location hints, which are best effort rather than guaranteed.
Shutdowns are normal. Objects restart for code updates, runtime updates, and placement decisions. WebSocket connections are dropped on shutdown, in-flight storage-touching requests are stopped, and the constructor runs again. Code that assumes an object lives for the duration of a game or a session will lose that state. Persist everything that matters, make clients reconnect with backoff, and treat in-memory variables as a cache.
Gates do not protect non-storage awaits. An outbound fetch() inside a read-modify-write opens a window in which another request runs. Either complete the storage read-modify-write before the call, or guard with explicit state such as a “pending” flag stored in SQLite. Using blockConcurrencyWhile() liberally is the opposite mistake: it blocks all events, including your own, and Cloudflare recommends using it sparingly, mainly for initialization.
Alarms retry, so side effects duplicate. An alarm handler that sends an email and then throws will be retried, up to six times with backoff, and send the email again. Record completion in storage and check it first, or use an idempotency key at the downstream service.
Billing traps. Forgetting a setInterval, using ws.accept() instead of hibernation, polling with short alarms across millions of objects, and writing a row per event are the four ways to turn a cheap design into an expensive one. Cloudflare advises scheduling an alarm only when there is work, since every invocation is billed.
No cross-object transactions and no global query. If a business rule spans two objects, such as transferring a balance between two accounts, you need a saga, a two-step protocol with compensating actions, or you must place both entities in one object. Reporting needs an export path, because you cannot run SQL across a million private databases.
Not a fit for hard real time or heavy compute. The default 30-second CPU budget per invocation is generous for coordination but unsuitable for batch number crunching, and network jitter rules out control loops.
Vendor coupling. The API is specific to Cloudflare. Wrapping storage and coordination behind a thin interface in your own code keeps migration possible, although the semantics (single global instance, input and output gates) are hard to reproduce elsewhere.
Practical Recommendations
Start by finding the atom of coordination. If you cannot name one entity per object whose operations must be ordered, you probably do not need Cloudflare Durable Objects. If you can, the rest follows.
Use SQLite-backed classes for everything new, set the migration tag with new_sqlite_classes, and run schema creation in the constructor so every cold start is self-healing. Use RPC methods rather than fetch() for internal calls, derive IDs with getByName() when the identity is meaningful, and read identity from ctx.id rather than trusting a caller-supplied value.
Make state durable first and fast second. Put authoritative data in SQLite, keep a cache in memory, and flush hot counters from alarms. Make alarm handlers idempotent. Prefer hibernatable WebSockets, and keep per-connection state in attachments. Add observability early, because the failure signatures (overloaded errors, SQLITE_FULL, unexpected restarts) are specific.
A short checklist before production:
- Each object has a clear key, and no object serves global traffic.
- Constructor creates schema and tolerates being run repeatedly.
- No
setInterval, no standardaccept()WebSockets, unless intentional. - Alarms are scheduled only when work exists and are idempotent.
- Hot writes are batched; indexes exist only for real queries.
- Location hints or jurisdictions chosen deliberately for latency or compliance.
- Clients reconnect with exponential backoff and resynchronise state.
- A cost model uses the published GB-second, request, and row rates, with a monthly alarm to re-check them.
- A recovery runbook uses bookmarks within the 30-day window.
Frequently Asked Questions
What are Cloudflare Durable Objects used for?
Cloudflare Durable Objects suit any workload where many clients must coordinate on shared state: collaborative editors, chat rooms, multiplayer game servers, per-tenant databases, rate limiters keyed by user, booking and inventory systems, and AI agent sessions. In each case one object per entity serializes operations, stores data in a private SQLite database, and can wake itself with an alarm. They are a poor fit for stateless request handling or heavy global fan-out.
How are Durable Objects different from Workers KV or D1?
Workers KV is an eventually consistent global key-value cache. D1 is a serverless SQL database queried from Workers. A Durable Object combines compute and storage in one single-threaded instance with strongly consistent storage, so your code runs next to the data and requests serialize. D1 suits shared relational queries across entities, while objects suit per-entity coordination. The pricing for SQLite rows matches D1.
What are the limits of a Durable Object?
For SQLite-backed objects, storage is 10 GB per object, a row or blob can be up to 8 MB, and CPU time is 30 seconds per invocation by default, configurable to 5 minutes. A single object has a soft limit of about 1,000 requests per second. Alarm handlers get 15 minutes of wall time. The number of objects is unlimited, with 500 classes per account on Paid.
Does a Durable Object stay running all the time?
No. It runs while requests, events, or pending I/O are in flight, then becomes idle. If it meets the hibernation criteria it leaves memory after about 10 seconds; otherwise it is evicted after roughly 70 to 140 seconds idle. Hibernated objects keep their WebSocket clients connected. When anything arrives, the constructor runs again and in-memory variables are gone, so persist important state in storage.
How much do Cloudflare Durable Objects cost?
Billing has compute and storage parts. On Workers Paid, requests are $0.15 per million after 1 million included, and duration is $12.50 per million GB-seconds after 400,000 included, at a fixed 128 MB. SQLite storage charges per row read and written, plus stored data above 5 GB-month. Hibernation, batching writes, and avoiding idle timers are the main levers. Check the pricing page for current rates.
Can I restore a Durable Object to an earlier state?
Yes, for SQLite-backed objects. You obtain a bookmark for the current moment or for a time within the last 30 days, call onNextSessionRestoreBookmark(), and restart the object, typically with ctx.abort(). The call returns a bookmark for the pre-restore state, so the restore can itself be reversed. It works per object, not across the namespace.
Further Reading
- Serverless cold starts: Lambda SnapStart vs Cloudflare Workers vs Cloud Run, for the stateless runtime that sits in front of every object.
- WebAssembly component model and wasmCloud edge architecture, for another take on placing logic near data.
- Linux 7.1 PREEMPT_RT vs Zephyr vs Xenomai real-time edge gateway tutorial, for the device side of a device-twin design.
- Cloudflare Durable Objects documentation, including the limits, pricing, lifecycle, alarms and rules pages cited above.
- Rules of Durable Objects, Cloudflare’s best-practice and anti-pattern guide.
By Riju — about
