Quanto custa um jogo online na Cloudflare: 364 mil requisições e uma guilda que não dormia
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:
- O plano pago custa no mínimo US$ 5 por mês e inclui 10 milhões de requisições, depois US$ 0,30 por milhão.
- Inclui também 30 milhões de milissegundos de CPU, depois US$ 0,02 por milhão. O Worker é cobrado por tempo de CPU, não por duração.
- Conta a requisição de entrada. Subrequisições não contam. Em WebSocket, só o
Upgradeinicial é uma requisição do Worker; as mensagens não entram nessa conta. - Arquivos estáticos são grátis.
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 evento | Quantidade | Parte do total |
|---|---|---|
| Mensagem de WebSocket recebida pelo objeto da guilda | 1.282 | 64,1% |
| HTTP no Worker | 634 | 31,7% |
| Chamadas internas Worker → Durable Object | 80 | 4,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:
| Fonte | Sozinho, aba visível | Em grupo | Aba em segundo plano |
|---|---|---|---|
| Presença (WebSocket) | 0,5 msg/s | 4 msg/s | ~1 msg/s, depois ~1/min |
| Dano ao chefão | 0,2 msg/s | 0,2 msg/s | 0,2 msg/s |
/api/profiles | ~0,5 req/s | 0,5 a 1,3 req/s | igual à visível |
| Save | 1 a cada 15–18 s | idem | ~1 por minuto |
version.txt | 1 a cada 2 min | idem | idem |
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:
- a presença chegava a cada 2 segundos;
- cada presença criava um timer de 0,5 segundo;
- 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):
| Linha | Hoje | 2× jogadores | 10× jogadores |
|---|---|---|---|
| Base do plano | US$ 5 | US$ 5 | US$ 5 |
| Requisições do Worker | US$ 0,39 | US$ 7,2 | US$ 48 |
| CPU | 0 | US$ 1,0 | US$ 7,4 |
| Durable Object: requisições | US$ 0,15 | US$ 0,5 | US$ 3,1 |
| Durable Object: duração | US$ 0,6 a 3,6 | US$ 7,9 | US$ 46 |
| Workers Logs | US$ 3 a 8 | US$ 35 | US$ 213 |
| Total | US$ 9 a 19 | US$ 45 a 60 | US$ 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
| Quando | Commit | O que mudou |
|---|---|---|
| 09/10, 22:14 | 2e81b08 | logs a 10%, nomes só de quem é novo, presença do grupo de 4 Hz para 1 Hz |
| 12/10, 19:25 | 500f4e1 | cache 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:48 | 5e420ab | grupo só roda com alguém olhando, presença solo a cada 15 s |
| 13/10, 11:53 | 3a5839c | grupo 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
- Não existe medição depois da correção. Os números de "hoje" depois do conserto são projeção, não leitura do painel.
- A amostra tem 8 minutos e 2 jogadores em grupo, o pior caso para a presença a 4 Hz.
- O tempo de hibernação (cerca de 10 s) é uma estimativa minha, não um número da documentação.
- A partir de 1º de dezembro de 2026, a página de preços diz que o Workers Logs passa para o preço do Cloudflare Observability. O US$ 0,60 por milhão deste artigo vale para novembro de 2026.
Como reproduzir
- No painel do Worker, exporte uma janela de eventos do Workers Logs (no meu caso, 2.000 eventos).
- Agrupe por método e caminho; some o que for WebSocket separado do HTTP.
- 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.
- Calcule a duração do Durable Object como
128 MB × segundos ativose confira a franquia na página de preços. - 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.