Cloudflare
Durable Objects
Arquitetura
Trade-offs
Decisão Técnica

When Durable Objects are the wrong answer

An honest analysis of when not to use Durable Objects, with the correct alternatives for each use case and the diagnostic question that simplifies the decision.

Durable Objects gain prominence because they solve something difficult — consistent state with serialized access at the edge — and this creates a bias: engineers who have just learned the tool tend to apply it to problems that it does not need to solve. The result is not bad code. It's correct, working code, more expensive and more complex than it needs to be. For a simple CRUD built on DOs when D1 would suffice, the bill could be between ten and fifty times larger, with none of the consistency benefit that the use case would require.

The question that determines whether you need DO

Before deciding on Durable Objects, there is a concrete question: does the state you need to manage require serialized access from multiple concurrent clients?

Serialized means that the order of operations matters and that two simultaneous operations on the same data must be mutually exclusive. Multiple competing clients means that more than one agent can attempt to modify the same data at the same time, and both agents need to see a consistent result.

If the answer is no — if the data is read by many but written rarely, or if concurrent writes to the same record are unlikely or handled by another layer — a DO adds complexity and cost without adding useful assurance.

Where D1 is the right answer

For relational data, D1 is the natural place within the Cloudflare stack. It has complete SQL, supports JOINs, indexes, transactions, ad hoc queries — everything that D1 has and DO doesn't have. A DO is a linearizable key-value store with procedural API. It has no way of answering "list all users who have logged in in the last 7 days and still have a positive balance" without you having explicitly pre-computed and stored this data.

D1 has much lower cost for read loads: $0.001/million lines read, $1.00/million lines written. The additional write latency to the primary bank region, which is the most cited disadvantage of D1, only affects write operations. For applications where reads dominate, this tradeoff is largely in favor of D1.

The confusing case: "but I need the writes to be atomic". D1 has transactions. BEGIN; UPDATE a...; UPDATE b...; COMMIT; guarantees atomicity. The difference is that D1 uses database locks — with the potential for contention at high concurrency — while a DO uses serialization by design. For the overwhelming majority of applications with normal write loads, D1 with transactions is sufficient and much cheaper.

Where KV is the right answer

KV is optimized for the pattern that dominates most edge use cases: lots of reads, occasional writes, no requirement for strong consistency between different PoPs. Configuration cache, pre-calculated compute results, user sessions where eventual consistency is acceptable — KV responds to local cache reads in the PoP with sub-millisecond latency and a pricing model that favors heavy reading.

The mistake is to assume that because KV doesn't have atomic read-modify-write operations, you need DO for anything that changes. The vast majority of data in web applications changes with patterns that do not require strict atomicity: the user profile that is updated when the user edits their preferences does not need mutual exclusion with another competing script — because no one else is editing the same profile at the same time.

DO starts to make sense when the frequency of concurrent writes to the same data is high enough that the eventual consistency of KV produces incorrect results visible to the user.

Rate limiting is not a DO use case

Rate limiting is the most common example of "it looks like it needs DO but it doesn't". The intuition is correct: rate limiting requires a counter per user that is incremented atomically with each request. If two Workers increment the same counter at the same time, there may be a race condition that lets a user make more requests than the limit.

The Cloudflare Rate Limiting API solves this natively, with no code, no DO, no additional cost beyond the plan. It is available on all paid plans, supports limits per IP, per authenticated user, per route, per header, and with configurable periods. For rate limiting at the edge, using the native API is simpler, cheaper and more reliable than implementing a counter DO.

The legitimate case for DO for rate limiting is when you have requirements that the native API does not cover: custom sliding window logic with exact precision, limits that depend on user session data that is not in headers, or rate limiting that needs to be consistent with other data that already lives in an application DO.

Workers Queues for asynchronous processing

Another pattern that sometimes appears as a candidate for DO: a work queue where multiple producers queue tasks and multiple consumers process. DO could model this as an object with a task list and a processing loop.

Workers Queues solve this natively: producers make env.QUEUE.send(message), consumers receive batches via handler, with automatic retry, dead letter queue, and separate charge of $0.40/million messages. For asynchronous processing with retries and backoff, Queues are better suited, cheaper, and do not have the serial throughput ceiling of a DO.

DO for asynchronous processing makes sense when consumption needs to be serialized by identity — processing all of a user's actions in order, without parallelism — and when that processing needs to access state that is local to the DO anyway.

File storage is R2, not DO storage

DO storage is a key-value store optimized for small objects — settings, counters, session state, messages. It has no documented limits by value, but is designed for data that can reasonably fit into a request. For files, images, videos, any larger blob, R2 is the right place: $0.015/GB-month of storage (less than a tenth of the cost of DO storage), no egress cost for Workers, with S3-compatible API.

DO storage is not a replacement for object storage — it is a transactional state store for DO itself.

Diagnosis before choosing

Four use cases where DO solves something that alternatives do not solve with the same guarantees: collaborative editing with multiple clients modifying the same document simultaneously, real-time presence coordination where the list of who is online needs to be consistent, distributed locks with timeout where expiration needs to be guaranteed even if the holder crashes, and event logs ordered by arrival where the insertion order is semantically important.

For everything that doesn't fit into this list, there is an alternative within the platform that is cheaper, simpler or both. The decision to use DO starts with the question about serialization. If you can't articulate why data access serialization is necessary for the use case, DO is probably not the tool.

The risk of using DO where it is not necessary is not that it breaks anything — the code will work. The cost is to carry unnecessary complexity: an abstraction that imposes serial throughput limits, requires Workers Paid plan as a prerequisite, has a distinct learning curve, and costs more than the alternatives for the cases that the alternatives solve well. For a team that already uses DOs elsewhere in the system, the marginal cost of one more DO is lower. For a team starting at Cloudflare with a simple CRUD, starting with D1 and KV and adding DOs where serialization is genuinely needed is the right order of complexity.

Also read