Cloudflare
Durable Objects
Custos
Produção
Limites

Durable Objects in production: what the bill will look like and the limits that surprise

Detailed analysis of the real costs of Durable Objects in production — how to calculate GB-seconds, where serial throughput becomes a bottleneck, and the limits that surprise at scale.

Durable Objects in production: what the bill will look like and the limits that surprise

Durable Objects pricing seems simple until you calculate the actual first month. Three interacting components, a free tier that seems generous until you measure the correct memory footprint, and a throughput ceiling that appears well before what most engineers expect when the production numbers arrive. Understanding the structure before putting it into production saves a surprise bill and an architectural redesign under pressure.

The tiered cost structure

Workers Paid plan is the prerequisite — $5/month, without it DOs are not available. From there:

Claims: $0.15/million after the first free million per month. Each call to a DO stub counts as a request. The Worker doing the routing uses the normal Workers request quota, separate from this.

Compute in GB-seconds: $12.50/million GB-seconds after 400K free per month. GB-second is the unit of work: memory used in GB multiplied by execution time in seconds. The minimum memory footprint per DO instance is 128MB (0.125 GB). A request that takes 10ms at 128MB consumes 0.00125 GB-seconds.

Storage: $0.20/GB-month after 1GB free. Cumulative — if you have 1,000 DOs with 10KB each, that's 10MB of storage, well within the free range. If you persist larger data per instance, the storage cost starts to appear.

Alarms: $0.15/million invocations. The same table as normal requests.

The real calculation of the free compute tier

400 thousand GB-seconds/month is the number. At a minimum footprint of 128MB per instance, this is equivalent to 3.2 million seconds of execution — approximately 889 hours of active DO per month, distributed among all your DOs.

The account that matters is by workload, not by DO. If you have DOs that process requests from 10ms to 128MB, each request consumes 0.00125 GB-seconds. The free tier covers 320 million of these requests — much more than the free tier of requests (1 million). The bottleneck of the free plan, for workloads with light and fast DOs, is the request quota, not the compute quota.

The scenario that reverses this account is DOs that maintain large state in memory. If a DO loads a 2MB document into memory to process collaborative edits, its footprint is not 0.125 GB — it's closer to 0.127 GB, plus the isolate overhead. For DOs that load large JSON, image buffers for processing, or large local caches, the actual GB-second grows with the size of the in-memory state, not the execution time of the request.

How much does a concrete workload cost? 1 million 100ms requests at 128MB: 100,000 GB-seconds = $1.25 in compute (within the free compute tier, but above the free tier of requests: $0.15 × (1 − 1) = free if it's the first million, $0.15 if it's the second). In total: $1.25 of compute + $0.15 of extra requests = $1.40 in addition to the $5 in the plan.

The throughput ceiling that appears earlier than expected

Serial execution guarantees the consistency of DOs and their throughput ceiling. A DO processes one request at a time. The maximum throughput per instance is inversely proportional to the average time per request.

The formula: maximum throughput (req/s) = 1000ms / average time per request (ms).

5ms processing per request: 200 req/s per DO instance. 10ms processing: 100 req/s. 50ms processing (including I/O as a call to an external service): 20 req/s.

This ceiling seems high until you have a popular feature that routes all requests to the same DO ID. A chat room with 500 messages per second being sent to the same DO will queue 300+ messages per second if the average processing time is 4ms. The latency perceived by customers increases proportionally to the size of the queue.

The solution is sharding. Instead of deriving the DO ID directly from the room or resource, you add a numeric suffix based on a hash of the identifier:

const shardCount = 10; const shard = Math.abs(hashCode(roomId)) % shardCount; const id = env.ROOMS.idFromName(`room-${roomId}-shard-${shard}`);

Each shard is an independent DO instance, with its own throughput ceiling. Sharding works well for operations where consistency needs to be guaranteed only within a shard — if you need global consistency across all shards of a resource, the solution becomes more complex.

Surprising storage limits

list() returns a maximum of 128 entries per call. This limit is not prominently documented, but it appears every time you have a DO with more than 128 keys in storage and call list() expecting the full set.

Pagination uses cursor:

async listAll(): Promise<Map<string, unknown>> { const result = new Map<string, unknown>(); let cursor: string | undefined; while (true) { const batch = await this.ctx.storage.list({ cursor, limit: 128 }); for (const [key, value] of batch) { result.set(key, value); } if (batch.size < 128) break; cursor = [...batch.keys()].at(-1); } return result; }

Ignoring this in development (where DOs have little data) and finding out in production with 500 keys is a bug that silently produces incorrect results — the application receives the first 128 items and acts as if they were all.

Another limit: transactions are confined to a single DO instance. There is no atomic transaction that modifies two different DO instances. If your design requires atomicity between two DOs — for example, transferring credit from one DO to another — you need to implement a two-phase commit protocol at the application layer, with compensation in case of failure. For most cases, this signals that the design needs to be revisited: either the state should live in the same DO, or the operation does not need strict atomicity between instances.

What to monitor after going into production

Response latency by DO ID is the most direct indicator of throughput bottleneck. If specific DOs have increasing latency while others become fast, those specific DOs are queuing requests — candidates for sharding.

Consumption of GB-seconds versus number of requests reveals DOs with a larger-than-expected memory footprint. If the GB-seconds/request ratio is growing without changing the processing time, some DO is loading more state into memory.

Storage by namespace shows growth in data that you may be accumulating without a cleanup routine. DOs that write to storage without ever deleting accumulate data indefinitely — $0.20/GB-month seems cheap until you have a few GB of historical data that no one accesses anymore.

Where $5/month makes more

The $5/month baseline covers the Paid plan. For small applications with usage within the free tiers, this is the total cost. For applications that grow, the marginal cost of the three dimensions — requests, compute, storage — grows in different ways depending on the type of load.

Frequent request loads but light processing: request tier runs out before compute. Solution: check if any of the requests can be cached in the Worker layer before reaching the DO.

Heavy processing loads with few requests: compute dominates. Solution: measure the actual memory footprint and average execution time, and check if part of the computation can be moved to the Worker that does the routing.

Many DOs with persisted data: storage dominates. Solution: set TTL for data that does not need to last indefinitely and implement cleanup routines via Alarm API.

The limits that are most prevalent in production are serial throughput and list(). The rest you can find in the documentation before being a surprise. These two you find in the first few days with real traffic.

Also read