Cloudflare
Durable Objects
Custos
Produção
Limites

Durable Objects em produção: o que a conta vai parecer e os limites que surpreendem

Análise detalhada dos custos reais de Durable Objects em produção — como calcular GB-segundos, onde o throughput serial vira gargalo, e os limites que surpreendem na escala.

O preço de Durable Objects parece simples até você calcular o primeiro mês real. Três componentes que interagem, um tier gratuito que parece generoso até você medir o footprint de memória correto, e um teto de throughput que aparece bem antes do que a maioria dos engenheiros espera quando os números de produção chegam. Entender a estrutura antes de botar em produção poupa uma surpresa na fatura e um redesign de arquitetura sob pressão.

A estrutura de custo em camadas

Workers Paid plan é o pré-requisito — $5/mês, sem ele DOs não estão disponíveis. A partir daí:

Requisições: $0.15/milhão após o primeiro milhão grátis por mês. Cada chamada ao stub de um DO conta como uma requisição. O Worker que faz o roteamento usa a cota de requisições normal de Workers, separada desta.

Compute em GB-segundos: $12.50/milhão de GB-segundos após 400 mil grátis por mês. GB-segundo é a unidade de trabalho: memória usada em GB multiplicada por tempo de execução em segundos. O footprint mínimo de memória por instância de DO é 128MB (0,125 GB). Uma requisição que leva 10ms a 128MB consome 0,00125 GB-segundos.

Storage: $0.20/GB-mês após 1GB grátis. Acumulativo — se você tem 1.000 DOs com 10KB cada, são 10MB de storage, bem dentro do grátis. Se você persiste dados maiores por instância, o custo de storage começa a aparecer.

Alarmes: $0.15/milhão de invocações. A mesma tabela das requisições normais.

O cálculo real do tier gratuito de compute

400 mil GB-segundos/mês é o número. No footprint mínimo de 128MB por instância, isso equivale a 3,2 milhões de segundos de execução — aproximadamente 889 horas de DO ativo no mês, distribuídas entre todos os seus DOs.

A conta que importa é por carga de trabalho, não por DO. Se você tem DOs que processam requisições de 10ms a 128MB, cada requisição consome 0,00125 GB-segundos. O tier gratuito cobre 320 milhões dessas requisições — muito mais do que o tier gratuito de requisições (1 milhão). O gargalo do plano gratuito, para cargas de trabalho com DOs leves e rápidos, é a cota de requisições, não a de compute.

O cenário que inverte essa conta é DOs que mantêm estado grande em memória. Se um DO carrega um documento de 2MB em memória para processar edições colaborativas, seu footprint não é 0,125 GB — é próximo de 0,127 GB, mais o overhead do isolate. Para DOs que carregam JSON grande, buffers de imagem para processamento, ou caches locais extensos, o GB-segundo real cresce com o tamanho do estado em memória, não com o tempo de execução da requisição.

Quanto custa uma carga de trabalho concreta? 1 milhão de requisições de 100ms a 128MB: 100.000 GB-segundos = $1.25 em compute (dentro do tier gratuito de compute, mas acima do tier gratuito de requisições: $0.15 × (1 − 1) = grátis se for o primeiro milhão, $0.15 se for o segundo). No total: $1.25 de compute + $0.15 de requisições extras = $1.40 além dos $5 do plano.

O teto de throughput que aparece antes do esperado

A execução serial é a garantia de consistência dos DOs e o seu teto de throughput. Um DO processa uma requisição por vez. O throughput máximo por instância é inversamente proporcional ao tempo médio por requisição.

A fórmula: throughput máximo (req/s) = 1000ms / tempo médio por requisição (ms).

Processamento de 5ms por requisição: 200 req/s por instância de DO. Processamento de 10ms: 100 req/s. Processamento de 50ms (se inclui I/O como chamada a um serviço externo): 20 req/s.

Esse teto parece alto até você ter uma feature popular que roteia todas as requisições para o mesmo DO ID. Uma sala de chat com 500 mensagens por segundo sendo enviadas ao mesmo DO vai enfileirar 300+ mensagens por segundo se o processamento médio for 4ms. A latência percebida pelos clientes sobe proporcional ao tamanho da fila.

A solução é sharding. Em vez de derivar o ID do DO diretamente da sala ou do recurso, você adiciona um sufixo numérico baseado num hash do identificador:

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

Cada shard é uma instância de DO independente, com seu próprio teto de throughput. O sharding funciona bem para operações onde a consistência precisa ser garantida apenas dentro de um shard — se você precisa de consistência global entre todos os shards de um recurso, a solução fica mais complexa.

Limites de storage que surpreendem

list() retorna no máximo 128 entradas por chamada. Esse limite não é documentado proeminentemente, mas aparece toda vez que você tem um DO com mais de 128 chaves na storage e chama list() esperando o conjunto completo.

A paginação usa 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; }

Ignorar isso em desenvolvimento (onde DOs têm poucos dados) e descobrir em produção com 500 chaves é um bug que produz resultados incorretos silenciosamente — a aplicação recebe os primeiros 128 itens e age como se fossem todos.

Outro limite: transações são confinadas a uma única instância de DO. Não existe transação atômica que modifica duas instâncias de DO diferentes. Se o seu design requer atomicidade entre dois DOs — por exemplo, transferir crédito de um DO para outro — você precisa implementar um protocolo de two-phase commit na camada de aplicação, com compensação em caso de falha. Para a maioria dos casos, isso sinaliza que o design precisa ser revisitado: ou o estado deve viver no mesmo DO, ou a operação não precisa de atomicidade estrita entre instâncias.

O que monitorar depois de botar em produção

Latência de resposta por DO ID é o indicador mais direto de gargalo de throughput. Se DOs específicos têm latência crescendo enquanto outros ficam rápidos, esses DOs específicos estão enfileirando requisições — candidatos a sharding.

Consumo de GB-segundos versus número de requisições revela DOs com footprint de memória acima do esperado. Se o ratio GB-segundos/requisição está crescendo sem mudança no tempo de processamento, algum DO está carregando mais estado em memória.

Storage por namespace mostra crescimento de dados que você pode estar acumulando sem rotina de cleanup. DOs que escrevem na storage sem nunca deletar acumulam dados indefinidamente — $0.20/GB-mês parece barato até você ter alguns GB de dados históricos que ninguém acessa mais.

Onde os $5/mês viram mais

O baseline de $5/mês cobre o plano Paid. Para aplicações pequenas com uso dentro dos tiers gratuitos, esse é o custo total. Para aplicações que crescem, o custo marginal das três dimensões — requisições, compute, storage — cresce de formas diferentes dependendo do tipo de carga.

Cargas de requisições frequentes mas processamento leve: o tier de requisições esgota antes do compute. Solução: checar se alguma das requisições pode ser cachada na camada do Worker antes de chegar ao DO.

Cargas de processamento pesado com poucas requisições: o compute domina. Solução: medir o footprint de memória real e o tempo médio de execução, e verificar se parte da computação pode ser movida para o Worker que faz o roteamento.

Muitos DOs com dados persistidos: storage domina. Solução: definir TTL para dados que não precisam durar indefinidamente e implementar rotinas de cleanup via Alarm API.

Os limites que mais pegam em produção são os de throughput serial e o de list(). Os demais você encontra na documentação antes de serem surpresa. Esses dois você encontra nos primeiros dias com tráfego real.

Leia também

Durable Objects em produção: o que a conta vai parecer e os limites que surpreendem | Matheus Breguêz