As gambiarras do plano grátis da Cloudflare
Vitor Ayres (abre em nova aba)
Um app interno de estoque de um distribuidor roda no plano grátis da Cloudflare: Worker, D1 e KV. É uma prova de conceito, e o plano grátis era a escolha certa para ela. O preço é um conjunto de limites que o código precisa contornar, e cada contorno é um pedaço de código que seria mais simples se o limite não existisse.
Este texto registra como mantemos esses contornos, as gambiarras, num documento com id, fonte do limite, data da leitura, custo e condição de saída, e o que medimos em cada um. Os limites foram lidos na documentação da Cloudflare em setembro de 2026 e conferidos de novo em 9 de outubro.
Resumo
- São 8 contornos ativos hoje. Eram 13 em setembro. Quatro sumiram quando os relatórios saíram do KV e foram para o D1, e um quando o SDK ganhou o que o app pedia.
- D1: 100 parâmetros por consulta, 50 consultas por requisição, sem transação interativa.
- Worker: 10 ms de CPU. Sete de nove chamadas medidas passaram disso, e nenhuma foi encerrada.
- KV: 1.000 escritas por dia, protegidas por uma política de oito uploads aceitos por hora.
- Uma página do ERP, de até 500 registros, por chamada de sincronização.
- Cada contorno tem id, fonte, data, custo e condição de saída.
- Um teste de 20 linhas falha quando código e documento discordam.
Por que um registro de gambiarras
A regra é simples: entra no registro todo contorno que existe por causa de um limite externo. Plano grátis, falta de recurso numa plataforma, restrição da máquina. Não entra dívida técnica em geral.
Os contornos só podem morar nos adaptadores, no Worker ou na borda do navegador. Nunca no núcleo do domínio. Quando um limite muda o desenho de uma interface do domínio, o item explica por quê. E como limites mudam, cada item diz a fonte e a data em que o número foi lido.
O plano grátis foi uma decisão com alternativas descartadas por motivo. A Vercel proíbe uso comercial no plano grátis. O Supabase pausa projetos parados. O Render dorme em 15 minutos e leva cerca de um minuto para acordar, o que seria toda visita. Neon trocaria o dialeto para Postgres, e Turso acrescentaria conta, segredo e dependência para tirar só os contornos do D1, que moram num arquivo.
A saída está decidida também. O Workers pago custa 5 dólares por mês e remove quase todos os contornos. É a primeira opção quando o limite de CPU atrapalhar de novo. A segunda é uma VPS, e o código já está pronto para ela: nada da Cloudflare fora dos adaptadores, SQL que roda em SQLite comum, migrações em .sql puro e um adaptador Node testado a cada commit.
D1: 100 parâmetros e 50 consultas
O D1 aceita no máximo 100 parâmetros por consulta, nos dois planos, e 50 consultas por invocação do Worker no plano grátis, 1.000 no pago. Os dois números estão na página de limites do D1.
A motivação é gravar uma página do ERP, de até 500 registros, com poucas consultas. Um INSERT por registro gastaria 500 consultas e estouraria o segundo limite. Então o adaptador faz INSERT de várias linhas, fatiado: cada fatia leva floor(100 / colunas) registros, 14 nas tabelas de 7 colunas e 16 na de 6. Uma página de 500 registros vira 36 consultas. O número de colunas vem do SQL da tabela, então acrescentar uma coluna reduz a fatia sozinho. Uma busca por muitos códigos vai em fatias de 80 valores, e os resultados são juntados e ordenados como uma consulta só devolveria.
Para o limite de 50, o adaptador anuncia quanto aceita por gravação: 14 vezes 48, ou 672 registros. São 48 comandos de registros, 1 de estado e 1 leitura. A sincronização para na primeira fronteira de página depois do teto, e uma página do ERP sempre cabe. Se alguém passar mais que o anunciado, a gravação falha antes de tocar no banco, com uma mensagem que cita o id da gambiarra.
Contamos cada comando de um batch como uma consulta. A documentação não diz o contrário, e é a leitura conservadora.
O D1 falso dos testes, em cima do SQLite do Node, impõe os dois limites. Tirar um contorno quebra um teste.
Uma alternativa não usada: mandar a página inteira como um parâmetro JSON e expandir com json_each. Seria uma consulta e um parâmetro. Não foi testada no D1, e o valor tem teto de 2 MB, perto do tamanho de uma página de clientes, cerca de 1,1 MB.
Sem transação: a porta em forma de lote
O D1 não mantém transação aberta entre chamadas. Não existe BEGIN, lógica em código, COMMIT. A única unidade atômica é batch(), uma lista de comandos já prontos.
Isso importa porque registros e cursor precisam gravar juntos. Um cursor à frente dos dados é perda silenciosa: a próxima leitura parte de um ponto que não foi salvo. Então a interface do domínio não oferece transação. Oferece gravar(recurso, registros, estado), e cada adaptador torna isso atômico do seu jeito: batch no D1, BEGIN e COMMIT no SQLite.
O custo é que toda operação atômica tem de caber num lote decidido antes de executar. Ler, decidir e gravar na mesma transação não existe. Onde isso fazia falta, a solução foi um token de revisão: o UPDATE do cabeçalho só casa com a revisão que a pessoa leu, e troca a revisão por um token único, que os comandos seguintes do lote exigem. Um contador não serviria, porque dois escritores chegariam ao mesmo número.
Essa gambiarra some com o D1, mas a porta em forma de lote pode ficar. É um desenho bom em qualquer banco.
CPU do Worker: o limite que não matou ninguém
O Workers grátis dá 10 ms de CPU por requisição. Espera de rede e de banco não conta. JSON.parse, laços e JSON.stringify contam.
Medimos antes de desenhar. Em 19 de setembro de 2026, em Node 24, uma página sintética de 500 clientes de 2,3 KB cada, 1,1 MB no total, passando por parse, mapeamento e serialização: 11,8 ms na primeira execução, fria; mediana de 6,3 ms aquecida; melhor caso 5,2 ms. Uma sincronização esporádica pega o isolate frio, então uma página cheia pode estourar os 10 ms sozinha. Por isso a sincronização processa uma página por chamada e devolve completo: false para quem chamou repetir.
Depois medimos no Worker de verdade, no mesmo dia, em produção no plano grátis, com wrangler tail. Uma amostra por rota:
| Rota | Registros | CPU |
|---|---|---|
| status do ERP | 0 | 25 ms |
| clientes | 1 | 16 ms |
| produtos | 383 | 13 ms |
| tabelas de preço | 8 | 31 ms |
| produtos por tabela, passo 1 | 500 | 8 ms |
| produtos por tabela, passo 2 | 500 | 24 ms |
| produtos por tabela, passo 3 | 388 | 5 ms |
| condições de pagamento | 0 | 17 ms |
| transportadoras | 1 | 17 ms |
Todas terminaram com outcome: ok. Nenhuma com exceededCpu.
O que a medição diz: sete de nove chamadas passaram de 10 ms e nenhuma foi encerrada, então o limite não é aplicado chamada a chamada com rigor. E o número não acompanha o volume: um registro custou 16 ms e 500 custaram 8 ms. O custo dominante parece ser fixo, do isolate frio e da primeira conexão de saída, e não o nosso parse. O que a medição não diz: é uma amostra por rota, a Cloudflare não documenta a tolerância, e uma página cheia de clientes de produção continua sem medição.
O corte é só entre páginas, porque dentro de uma página a ordem não é garantida. Cada corte desperdiça uma requisição ao ERP: para ver que a página acabou é preciso puxar o primeiro registro da seguinte, que é descartado e relido na próxima chamada.
Duas outras coisas saíram do Worker pelo mesmo limite. O parse da planilha de estoque mede de 25 a 55 ms em Node, então roda no navegador, e o servidor só valida o JSON e guarda a planilha original para reprocessar. E a primeira versão do cache do catálogo, um JSON único no KV mesclado por id, foi descartada no mesmo dia: a mescla media 6 ms com 600 registros e de 23 a 35 ms com 2.000.
KV: mil escritas por dia, e quatro gambiarras que sumiram
O KV grátis aceita 1.000 escritas por dia. Um upload aceito gravava quatro chaves, e a conta era 8 uploads por hora, vezes 24, vezes 4: 768 escritas, com folga de 232. A política de oito uploads aceitos por hora ficou, agora decidida na mesma transação que publica a versão, no D1.
Em 26 de setembro, o histórico de relatórios saiu do KV e foi para o D1. Quatro gambiarras sumiram com isso: a chave separada para a versão atual, por causa da consistência eventual do KV, que podia devolver um índice de até 60 segundos atrás; a releitura do índice logo antes de gravar, porque o KV não tem transação nem escrita condicional; a verificação de que uma versão “não encontrada” existia de fato; e a espera de 1,1 e 2,2 segundos depois de um 429, porque o KV aceita uma escrita por segundo na mesma chave.
Ficou um custo aceito: tentativas simultâneas podem gravar arquivos antes de disputar a última vaga, e uma publicação que falha pode deixar um arquivo sem referência. A cota de uploads não é um teto absoluto de escritas no KV.
As que ainda não precisaram existir
O D1 cobra linha lida, não linha devolvida, e o plano grátis dá 5 milhões de linhas por dia. Uma busca com LIKE '%texto%' não usa índice e lê a tabela inteira. Com 2.000 clientes e 500 buscas por dia seriam 1 milhão de linhas, um quinto da cota. É uma projeção, não uso medido, e por isso o contorno ainda não existe. Quando a tabela passar de alguns milhares de linhas, as opções são espera entre teclas, busca por prefixo com índice, ou FTS5.
Uma gambiarra que já saiu era do nosso próprio SDK, não da Cloudflare. O list() do mercos-sdk devolvia um fluxo de registros e escondia onde uma página termina, e o corte por página precisa dessa fronteira. O app contava as requisições para descobrir. O SDK ganhou um listPages() na versão 0.4.0, o app migrou em 9 de outubro, e o contador, a seção no documento e o desperdício de uma requisição por corte saíram juntos. Como esse app moldou o SDK está no post sobre o SDK e o seu primeiro app.
O teste que cobra a marca
Cada contorno leva no código o comentário GAMBIARRA(id). Um teste percorre src, web, scripts e a extensão, coleta os ids e os compara com as seções do documento e com a tabela de mapa no topo dele. Ele falha em três casos: id no código sem seção, seção sem id no código, ou tabela e seções diferentes.
Hoje são 8 ids no código e 8 seções. Ao remover uma gambiarra, a seção e o comentário saem juntos, senão o teste avisa.
Como verificamos
Os limites foram lidos na documentação da Cloudflare em 19 de setembro de 2026 para o D1 e em 26 de setembro para o KV, e conferidos de novo em 9 de outubro. A CPU foi medida em 19 de setembro com wrangler tail --format json, no Worker em produção no plano grátis, contra o ambiente de testes do ERP, uma amostra por rota. A medição em Node foi no mesmo dia, com uma página sintética de 1,1 MB.
O que não medimos: uma página cheia de produção no Worker, a tolerância do limite de CPU, e o uso da cota de leituras do D1.
O app é de um cliente e não é público. O ERP é o Mercos, e o que a API dele faz está no post sobre a API do Mercos. O mesmo Worker serve o servidor MCP do app.
Se a sua empresa precisa ligar um ERP a um sistema próprio, com sincronização que retoma de onde parou, é esse o trabalho que fazemos em integrações. O SDK que o app usa é código aberto.
Perguntas
- Quantas consultas cabem numa requisição do D1 no plano grátis?
- 50 por invocação do Worker, contra 1.000 no plano pago. Contamos cada comando de um batch como uma consulta, que é a leitura conservadora da documentação.
- O Worker grátis mata a requisição que passa de 10 ms de CPU?
- Não sempre. Em nove chamadas medidas em produção, sete passaram de 10 ms e nenhuma foi encerrada. A Cloudflare não documenta a tolerância, então não dá para contar com ela.
- Quantos parâmetros cabem numa consulta do D1?
- 100, nos dois planos. Uma página de 500 registros vira 36 INSERTs de várias linhas, com 14 registros cada quando a tabela tem 7 colunas.
- Quanto custa sair dos limites?
- O Workers pago custa 5 dólares por mês e remove quase todos os contornos. A outra saída é uma VPS: o código já roda em Node com SQLite, com o mesmo SQL.
- Como garantir que nenhum contorno fica esquecido no código?
- Cada um tem uma marca GAMBIARRA(id) no código e uma seção no documento. Um teste de 20 linhas falha se a marca existe sem seção, ou a seção sem marca.