Workers são stateless por design. Cada requisição chega num isolate limpo, sem memória do que aconteceu antes, sem estado compartilhado com outras instâncias rodando em paralelo. Esse isolamento é exatamente o que permite escalar milhões de requisições sem coordenação entre instâncias. Durable Objects quebram esse contrato intencionalmente — e entender por que, e o que exatamente muda, determina se você vai usá-los certo ou sofrer com eles.
O que um Durable Object é, de fato
Um Durable Object é uma classe JavaScript com armazenamento persistente e um detalhe de execução que muda tudo: as requisições chegam serialmente. Não há concorrência dentro de uma instância. Enquanto o método fetch() de um DO está processando uma requisição, todas as outras que chegam ao mesmo DO ficam enfileiradas do lado de fora.
Isso elimina toda uma categoria de bugs que surgem do acesso concorrente a estado compartilhado. Não existe condição de corrida dentro de um DO. Se você incrementa um contador, persiste o valor e responde, nenhuma outra requisição pode ter interleaved entre essas operações. A sequência é garantida pela plataforma.
Cada instância de DO existe num único Cloudflare PoP — o ponto de presença mais próximo da primeira requisição que a criou. Requisições subsequentes ao mesmo DO são roteadas para esse PoP específico, independentemente de onde o cliente está. Se a sua instância foi criada em Frankfurt e um cliente de São Paulo faz uma requisição a ela, a requisição viaja até Frankfurt. Isso tem implicações reais de latência para casos de uso geograficamente distribuídos, e é importante conhecer antes de projetar.
Por que KV e D1 não resolvem o mesmo problema
A comparação natural é com KV e D1, as outras opções de persistência na plataforma. A diferença não é de conveniência — é de modelo de consistência.
KV é eventually consistent. Escrita num PoP propaga para os outros com um atraso mensurável, tipicamente entre alguns milissegundos e um minuto. Leituras em diferentes PoPs podem ver versões diferentes do mesmo valor. Para cache de leitura, para configurações que mudam raramente, KV funciona perfeitamente. Para qualquer operação que exige "ler, computar, escrever" com garantia de que nenhuma outra escrita aconteceu no meio — não funciona.
D1 com transações resolve a atomicidade para leituras e escritas, mas introduz latência de cross-region para escritas. Toda escrita vai para a região primária do banco, que pode estar num PoP diferente do seu Worker. Para muitas aplicações isso é aceitável. Para estado que precisa ser modificado com baixa latência e alta frequência, ou para coordenação em tempo real entre clientes, o custo de latência de cada write para uma região primária muda o problema.
Um DO mantém estado em memória e persiste atomicamente via a API de storage. A operação await this.ctx.storage.put('count', this.count) é linearizável: qualquer requisição posterior ao mesmo DO vai ver o valor que foi escrito, sem exceção. Essa garantia, combinada com a execução serial, é o que torna possível construir contadores atômicos, locks distribuídos, logs de eventos ordenados e coordenação entre clientes concorrentes sem a complexidade de implementar controle de concorrência na aplicação.
Como a execução serial afeta throughput
A execução serial é a garantia e o gargalo ao mesmo tempo. Um DO que processa cada requisição em 5ms consegue tratar aproximadamente 200 requisições por segundo. Se a sua aplicação roteia 500 req/s para o mesmo DO, 300 delas ficam enfileiradas e somam latência.
Esse teto existe por design. Se um único DO virar gargalo, a solução é sharding: em vez de um ID fixo para um recurso, distribuir por sufixo numérico. Para um recurso identificado por userId, a estratégia user-{userId}-shard-{userId.charCodeAt(0) % 10} distribui a carga entre dez instâncias, cada uma com seu próprio teto de throughput. A escolha de qual shard usar precisa ser determinística para que leituras e escritas do mesmo cliente sempre cheguem à mesma instância.
A hibernação muda o modelo de custo de manter muitas instâncias ativas. Quando um DO não tem requisições pendentes, ele hiberna automaticamente — sem custo de compute enquanto hibernado. O wake-up da hibernação leva sub-milissegundos. Para aplicações com muitos DOs esparsos (um por usuário, um por sala, um por sessão), o custo real de compute é proporcional ao uso ativo, não ao número total de instâncias.
O que o tier de preços exige de você
Durable Objects exigem o Workers Paid plan, com mínimo de $5/mês. A partir daí, o custo tem três componentes: requisições ($0.15/milhão, com 1 milhão grátis/mês), compute em GB-segundos ($12.50/milhão de GB-segundos, com 400 mil grátis/mês) e storage ($0.20/GB-mês, com 1GB grátis).
O GB-segundo é a parte que confunde. Um DO rodando com 128MB de memória por 1 segundo consome 0,125 GB-segundos. Com 400 mil GB-segundos grátis por mês, isso equivale a 3,2 milhões de segundos de execução no footprint mínimo. Uma requisição média de 10ms a 128MB consome 0,00125 GB-segundos, então o tier grátis cobre aproximadamente 320 milhões de requisições de 10ms no mínimo de memória. Se o seu DO faz computação pesada, mantém estado grande em memória ou processa muitas requisições por segundo, o consumo de GB-segundos sobe proporcionalmente.
Outra peça do custo: Alarm API. O DO pode agendar a execução de um método alarm() numa data futura — mesmo se hibernar no intervalo. O custo é $0.15/milhão de invocações de alarme, a mesma tabela das requisições normais.
Quando escolher Durable Objects
A pergunta que determina se DO é a ferramenta certa é direta: o estado que você precisa gerenciar exige acesso serializado de clientes concorrentes? Se sim, DO. Se não, existe uma opção mais simples e mais barata.
Casos em que DO resolve algo que nada mais na plataforma resolve com a mesma garantia: contadores atômicos com alta frequência de escrita, coordenação de presença em tempo real (quem está conectado numa sala), filas de eventos ordenados por chegada, locks distribuídos com timeout via Alarm API, e sessões de edição colaborativa onde múltiplos clientes modificam o mesmo documento.
O que não é caso de uso de DO: armazenamento de dados relacionais com queries ad hoc (D1 é muito mais adequado), cache de leitura com escritas ocasionais (KV é mais barato e globalmente distribuído), armazenamento de arquivos (R2), e qualquer estado que naturalmente mapeia para uma única escrita sem leituras concorrentes que dependam do estado anterior.
O que monitorar em produção
Dois indicadores que aparecem cedo em problemas com DOs: latência de fila crescente (sinal de que um DO virou gargalo e precisa de sharding) e consumo de GB-segundos crescendo além do esperado (sinal de que DOs estão mantendo estado em memória maior do que o necessário, ou sendo mantidos ativos com trabalho desnecessário).
A API de storage tem um detalhe operacional que pega de surpresa: list() retorna no máximo 128 entradas por chamada por padrão. Para conjuntos de dados maiores, a iteração precisa usar cursor. Ignorar isso em desenvolvimento e descobrir em produção com mil chaves tem custo real de requisições e de tempo de resposta.
A decisão de usar DO ou não é menos sobre performance e mais sobre qual garantia de consistência a sua aplicação exige. Entender esse requisito antes de escolher a ferramenta poupa uma migração cara depois.
Leia também
- O modelo de programação dos Durable Objects: o que é diferente de tudo que você já usou
- Durable Objects em produção: o que a conta vai parecer e os limites que surpreendem
- Durable Objects e WebSockets: multiplayer sem servidor dedicado
- Quando Durable Objects são a resposta errada
- Cloudflare KV: o que globalmente distribuído significa quando você precisa escrever
- Cloudflare Workers vs Pages: a diferença que importa antes de você escolher