Custo e capacidade

Quanto custa um jogo online na Cloudflare: 364 mil requisições e uma guilda que não dormia

Marcone Santos · 4 de novembro de 2026 · 8 min de leitura

Na noite do lançamento o painel mostrou 364 mil requisições em 24 horas para 42 heróis. Medi de onde vinha, fiz a conta por jogador e projetei a fatura em 1×, 2× e 10×.

Na noite de 9 de outubro de 2026, o meu jogo estava no ar havia menos de um dia e o painel da Cloudflare mostrava 364 mil requisições HTTP em 24 horas. O banco tinha 42 heróis. Este artigo mostra como achei o culpado, a conta por jogador conectado e a projeção da fatura. Todos os números são de uma medição daquele dia, não de uma fatura real, e dizem onde estão as dúvidas.

O que você vai encontrar aqui e que a documentação não mostra: a conta de custo por aba aberta de um jogo pequeno, feita a partir de uma amostra real de log, com a projeção da fatura ao lado.

O que o Worker cobra de verdade

Minha primeira reação foi achar que eu pagava por requisição. A página de preços do Workers (conferida em 04/11/2026) mostra outra coisa:

Com isso, 364 mil por dia dá cerca de 11,3 milhões por mês: 1,3 milhão acima da franquia, a US$ 0,30 o milhão. Cerca de US$ 0,39. O susto era grande, e a conta era de centavos. O problema nunca foi esse número; foi para onde ele apontava.

Achar a rota

O painel só conta HTTP. Para ver o que estava por trás, exportei 2.000 eventos de log de uma janela de 8 minutos e meio (18:31 a 18:40, horário de Brasília) e agrupei por rota.

Tipo de eventoQuantidadeParte do total
Mensagem de WebSocket recebida pelo objeto da guilda1.28264,1%
HTTP no Worker63431,7%
Chamadas internas Worker → Durable Object804,0%

Das 634 requisições HTTP, 556 (87,7%) eram GET /api/profiles, a rota que devolve o nome dos jogadores. Em 548 delas, os ids pedidos eram os mesmos 28. A amostra tinha 6 endereços diferentes, e dois jogadores (um no Mac, outro no Windows) fizeram 591 das 634 requisições, cerca de 0,6 por segundo cada.

Por que cada aba pedia os mesmos nomes

O cliente chamava refreshNames() toda vez que o servidor mandava a lista de quem estava online. Esse código, antes da correção:

async function refreshNames() {
  if (!backend.online || namesBusy) return;
  const ids = new Set(R.members.map(m => m.id));
  for (const p of R.peers) { const id = p.by || (p.presence && p.presence.uid); if (id) ids.add(id); }
  if (!ids.size) return;
  namesBusy = true;
  const ps = await backend.people.profiles([...ids]);

Sem memória de quem já tinha sido buscado, cada atualização de presença disparava a rota de novo, entre meia e uma vez por segundo, por aba. O navegador reduz os timers de uma aba em segundo plano, mas a mensagem do servidor chega do mesmo jeito, e a mensagem era o gatilho. A aba escondida pedia na mesma taxa da visível.

A correção, no commit 2e81b08 (09/10, 22:14), busca só ids desconhecidos ou vencidos:

const now = Date.now(), want = [...ids].filter(id => !(now - (R.namesAt[id] || 0) < NAMES_TTL));
if (!want.length) return;

A conta por jogador conectado

Lendo o código e conferindo na amostra, a cadência de cada aba na época era esta:

FonteSozinho, aba visívelEm grupoAba em segundo plano
Presença (WebSocket)0,5 msg/s4 msg/s~1 msg/s, depois ~1/min
Dano ao chefão0,2 msg/s0,2 msg/s0,2 msg/s
/api/profiles~0,5 req/s0,5 a 1,3 req/sigual à visível
Save1 a cada 15–18 sidem~1 por minuto
version.txt1 a cada 2 minidemidem

Somando as 24 horas, isso equivalia a 5 a 8 abas conectadas em média. Não eram 42 jogadores ao mesmo tempo, eram poucas abas fazendo muito barulho.

A guilda que não dormia

Cada guilda é um Durable Object. A duração é cobrada em tempo de relógio enquanto o objeto está ativo, ou ocioso mas sem poder hibernar: 128 MB × 86.400 s = 11.059 GB-s por dia, ou cerca de 343 mil GB-s por mês, antes da franquia de 400 mil. Fora a franquia, US$ 4,29 por mês por guilda acordada o dia inteiro.

O objeto só hiberna depois de uns 10 segundos sem evento e sem timer (valor que usei como estimativa; não está documentado). Aqui isso nunca acontecia:

  1. a presença chegava a cada 2 segundos;
  2. cada presença criava um timer de 0,5 segundo;
  3. o grupo rodava um intervalo de 4 vezes por segundo.

E havia um erro meu: a função que lista os jogadores do grupo exigia pv === 2 && cls && uid, mas não olhava se a aba estava visível. Uma aba esquecida mantinha a guilda acordada. Em 13/10 desliguei o grupo ao vivo inteiro por outro motivo (a complexidade custava mais do que valia), e o objeto da guilda passou de 548 para 29 linhas.

A projeção: 1×, 2× e 10×

Com os preços da página oficial, projetei a fatura mensal (ordem de grandeza, sem ter mudado nada):

LinhaHoje2× jogadores10× jogadores
Base do planoUS$ 5US$ 5US$ 5
Requisições do WorkerUS$ 0,39US$ 7,2US$ 48
CPU0US$ 1,0US$ 7,4
Durable Object: requisiçõesUS$ 0,15US$ 0,5US$ 3,1
Durable Object: duraçãoUS$ 0,6 a 3,6US$ 7,9US$ 46
Workers LogsUS$ 3 a 8US$ 35US$ 213
TotalUS$ 9 a 19US$ 45 a 60US$ 250 a 350

O que escala mal, nesta ordem: o log de cada requisição, as requisições da rota de nomes e a duração dos objetos acordados. O banco D1 (menos de 2% da franquia de leitura) não pesa nem em 10×. No plano pago, passou da franquia, o excedente vira cobrança por unidade: o risco não é o jogo parar, é a fatura chegar maior.

Com as três correções principais (log amostrado, nomes só de quem é novo, presença do grupo a 1 Hz), a mesma projeção cai para US$ 6 a 8 hoje, US$ 15 em 2× e US$ 50 a 70 em 10×.

O que eu corrigi, e em que ordem

QuandoCommitO que mudou
09/10, 22:142e81b08logs a 10%, nomes só de quem é novo, presença do grupo de 4 Hz para 1 Hz
12/10, 19:25500f4e1cache do número que o servidor calculava a cada save, chefão gravado em lote, recuo de 3 min depois de um save recusado
12/10, 19:485e420abgrupo só roda com alguém olhando, presença solo a cada 15 s
13/10, 11:533a5839cgrupo ao vivo desligado inteiro

O commit das 19:48 durou menos de um dia: no seguinte o grupo saiu, e a otimização deixou de ter o que frear. Tenho um alerta de fatura em US$ 20 por mês, configurado, que nunca disparou.

O que eu não sei

Como reproduzir

  1. No painel do Worker, exporte uma janela de eventos do Workers Logs (no meu caso, 2.000 eventos).
  2. Agrupe por método e caminho; some o que for WebSocket separado do HTTP.
  3. Para cada rota, divida pela quantidade de clientes distintos e pelo tempo da janela. Se uma rota passa de 0,5 requisição por segundo por aba, desconfie.
  4. Calcule a duração do Durable Object como 128 MB × segundos ativos e confira a franquia na página de preços.
  5. Projete com os preços da página oficial, em três cenários, e deixe escrito qual é medição e qual é projeção.

O que eu faria diferente

Mediria as requisições por rota desde o primeiro dia, não depois do susto. Testaria uma aba escondida antes de lançar, porque o gatilho vinha de uma mensagem que o navegador não freia. E criaria o alerta de fatura antes do lançamento, não depois.

Quer ver isso rodando? Os números e o código deste artigo vêm da Guilda do Ócio, um RPG idle cooperativo para devs, grátis e no navegador. Os vídeos desta série estão no YouTube e no Instagram.