Cloudflare
Durable Objects
Estado
Concorrência
Serverless

Cloudflare Durable Objects: Consistent state at the edge — what really changes

What Durable Objects actually change in Cloudflare's state model, how serial execution ensures consistency, and what it actually costs.

Cloudflare Durable Objects: Consistent state at the edge — what really changes

Workers are stateless by design. Each request arrives in a clean isolate, with no memory of what happened before, no state shared with other instances running in parallel. This isolation is exactly what allows you to scale millions of requests without coordination between instances. Durable Objects break this contract intentionally — and understanding why, and what exactly changes, determines whether you will use them right or suffer from them.

What a Durable Object actually is

A Durable Object is a JavaScript class with persistent storage and an execution detail that changes everything: requests arrive serially. There is no competition within an instance. While method fetch() of a DO is processing a request, all others that arrive at the same DO are queued outside.

This eliminates a whole category of bugs that arise from concurrent access to shared state. There is no race condition within a DO. If you increment a counter, persist the value and respond, no other requests can be interleaved between these operations. The sequence is guaranteed by the platform.

Each DO instance exists in a single Cloudflare PoP — the point of presence closest to the first request that created it. Subsequent requests to the same DO are routed to that specific PoP, regardless of where the client is. If your instance was created in Frankfurt and a client from São Paulo makes a request to it, the request travels to Frankfurt. This has real latency implications for geographically distributed use cases, and is important to know before designing.

Why KV and D1 don't solve the same problem

The natural comparison is with KV and D1, the other persistence options on the platform. The difference is not one of convenience — it is one of consistency model.

KV is eventually consistent. Writing to one PoP propagates to others with a measurable delay, typically between a few milliseconds and a minute. Readings in different PoPs may see different versions of the same value. For read cache, for settings that change rarely, KV works perfectly. For any operation that requires "read, compute, write" with a guarantee that no other writing has happened in between — it doesn't work.

D1 with transactions resolves atomicity for reads and writes, but introduces cross-region latency for writes. All writing goes to the bank's primary region, which may be in a different PoP than your Worker. For many applications this is acceptable. For state that needs to be modified with low latency and high frequency, or for real-time coordination between clients, the latency cost of each write to a primary region changes the problem.

A DO maintains state in memory and persists atomically via the storage API. Operation await this.ctx.storage.put('count', this.count) is linearizable: any subsequent request to the same DO will see the value that was written, without exception. This guarantee, combined with serial execution, is what makes it possible to build atomic counters, distributed locks, ordered event logs, and coordination between concurrent clients without the complexity of implementing concurrency control in the application.

How serial execution affects throughput

Serial execution is the guarantee and the bottleneck at the same time. A DO that processes each request in 5ms can handle approximately 200 requests per second. If your application routes 500 req/s to the same DO, 300 of them are queued and add latency.

This ceiling exists by design. If a single DO becomes a bottleneck, the solution is sharding: instead of a fixed ID for a resource, distribute it by numeric suffix. For a resource identified by userId, strategy user-{userId}-shard-{userId.charCodeAt(0) % 10} distributes the load among ten instances, each with its own throughput ceiling. The choice of which shard to use needs to be deterministic so that reads and writes from the same client always reach the same instance.

Hibernation changes the cost model of keeping many instances active. When a DO has no pending requests, it automatically hibernates — no compute costs while hibernated. Wake-up from hibernation takes sub-milliseconds. For applications with many sparse DOs (one per user, one per room, one per session), the actual compute cost is proportional to active usage, not the total number of instances.

What the price tier demands of you

Durable Objects require the Workers Paid plan, with a minimum of $5/month. From there, the cost has three components: requests ($0.15/million, with 1 million free/month), compute in GB-seconds ($12.50/million GB-seconds, with 400 thousand free/month) and storage ($0.20/GB-month, with 1GB free).

The GB-second is the confusing part. A DO running with 128MB of memory for 1 second consumes 0.125 GB-seconds. With 400,000 free GB-seconds per month, this is equivalent to 3.2 million seconds of execution on the minimum footprint. An average 10ms request at 128MB consumes 0.00125 GB-seconds, so the free tier covers approximately 320 million 10ms minimum memory requests. If your DO performs heavy computation, maintains a large state in memory or processes many requests per second, the GB-second consumption increases proportionally.

Another cost piece: Alarm API. DO can schedule a alarm() method to run at a future date — even if it hibernates in between. The cost is $0.15/million alarm invocations, the same table as normal requests.

When to choose Durable Objects

The question that determines whether DO is the right tool is straightforward: does the state you need to manage require serialized access from competing clients? If yes, DO. If not, there is a simpler and cheaper option.

Cases in which DO solves something that nothing else on the platform solves with the same guarantee: atomic counters with high writing frequency, real-time presence coordination (who is connected to a room), event queues ordered by arrival, distributed locks with timeout via Alarm API, and collaborative editing sessions where multiple clients modify the same document.

What is not a DO use case: relational data storage with ad hoc queries (D1 is much better suited), read caching with occasional writes (KV is cheaper and globally distributed), file storage (R2), and any state that naturally maps to a single write without concurrent reads that depend on the previous state.

What to monitor in production

Two indicators that appear early in problems with DOs: increasing queue latency (a sign that a DO has become a bottleneck and needs sharding) and consumption of GB-seconds growing beyond expectations (a sign that DOs are maintaining state in memory longer than necessary, or being kept active with unnecessary work).

The storage API has an operational detail that catches you by surprise: list() returns a maximum of 128 entries per call by default. For larger datasets, iteration needs to use cursor. Ignoring this in development and finding out in production with a thousand keys has a real cost in requests and response time.

The decision to use DO or not is less about performance and more about what consistency guarantee your application requires. Understanding this requirement before choosing the tool saves an expensive migration later.

Also read