A experiência de desenvolvimento com D1 é genuinamente boa: um SQLite local que o wrangler dev cria automaticamente, queries que respondem em poucos milissegundos, sem servidor para configurar, sem string de conexão para gerenciar. Esse atrito zero no desenvolvimento tende a criar uma ilusão de que o banco vai se comportar da mesma forma em produção. Não vai. O teto de 2GB por banco, os subrequests que somam, o custo de linhas escritas por UPDATE e a enforcement de foreign keys que exige opt-in manual por sessão são os quatro limites que aparecem em produção e que o quickstart nunca menciona.
O teto de 2GB que ninguém planeja
O D1 impõe um limite de 2GB por banco de dados. Esse número é fixo — não existe opção de aumentar para um banco específico comprando capacidade adicional. O plano pago permite até 10 bancos D1, o que significa um total teórico de 20GB distribuídos entre instâncias separadas.
Para uma aplicação CRUD simples com volume modesto de dados, 2GB é suficiente por anos. Para aplicações com logs, histórico de eventos, uploads de dados de usuário ou tabelas que crescem com o uso, o limite aparece mais cedo do que o esperado. O problema não é chegar a 2GB em si — é o momento em que você percebe que vai chegar, com dados em produção e sem uma estratégia de particionamento definida.
O caminho mais limpo para aplicações com crescimento previsível é particionar dados por domínio desde o início: um banco para dados transacionais ativos, outro para histórico, outro para logs. Uma aplicação SaaS pode particionar por tenant ranges — tenants 1 a 1000 no banco A, 1001 a 2000 no banco B. O Worker decide qual banco acessar com base no ID do tenant, sem que o usuário perceba. Essa arquitetura precisa ser pensada antes de os primeiros dados chegarem, porque refatorar o particionamento com um banco em produção próximo do limite é uma operação delicada.
O custo oculto dos subrequests
Cada query executada no D1 conta como um subrequest no orçamento do Worker. O limite de Workers é 1000 subrequests por invocação. Isso parece espaçoso até você mapear o que um único request HTTP faz: autenticar o token (1 query), carregar o usuário (1 query), verificar permissões (1 query), buscar a lista de recursos (1 query), e assim por diante. Dez queries em um endpoint é comum.
O problema de N+1 transforma esse número rapidamente. Um endpoint que lista 50 pedidos e depois busca os itens de cada pedido individualmente executa 1 + 50 = 51 queries. Combinado com 5 leituras de KV para cache e 3 acessos ao R2 para metadados, esse único request usa 59 subrequests. Ainda dentro do limite, mas com margem pequena para endpoints mais complexos.
O db.batch() resolve o N+1 sem mudar a estrutura dos dados: você agrupa múltiplas queries em uma única chamada, e todas executam em um único subrequest. O resultado volta como um array com um elemento por query. Para o padrão de pedidos e itens, db.batch() com as queries construídas dinamicamente reduz 51 subrequests para 2 — uma query para os pedidos, uma com IN para todos os itens de uma vez.
Write amplification: o que $1/milhão de linhas escritas realmente significa
O modelo de cobrança do D1 para escritas é por linha afetada, não por operação. Um UPDATE que modifica 5.000 linhas custa 5.000 escritas, independente de ser uma única chamada ao banco. A $1 por milhão de linhas escritas, esse UPDATE custa $0,005 por execução.
Esse número parece pequeno, mas operações em lote têm efeito acumulativo. Um job diário que atualiza status de 100 mil registros como parte de um processamento noturno custa $0,10 por execução, $3 por mês apenas para esse job. Multiplicado por vários jobs semelhantes, o custo de escritas pode superar o custo de leituras facilmente.
O tier gratuito tem um limite de 100 mil linhas escritas por dia. Uma única operação de atualização em lote pode consumir esse limite inteiro. Isso significa que o tier gratuito não é compatível com pipelines de processamento que fazem bulk updates — para qualquer carga de trabalho com escritas em volume, o plano pago é o único caminho.
Padrões que reduzem o custo de escritas: append-only em vez de update (inserir um novo registro de estado em vez de atualizar o existente), compactação periódica em vez de updates contínuos, e processar em lotes maiores com menor frequência em vez de updates granulares e frequentes.
FOREIGN KEYS e PRAGMA: a pegadinha por sessão
SQLite não enforce foreign keys por padrão. Esse comportamento é herdado pelo D1 sem modificação. Se você define FOREIGN KEY (user_id) REFERENCES users(id) no schema e insere uma linha com um user_id que não existe na tabela users, o D1 aceita o INSERT sem erro — a menos que você tenha executado PRAGMA foreign_keys = ON naquela sessão.
O detalhe crítico é "naquela sessão". O PRAGMA não persiste entre conexões. Cada invocação de Worker que precisa de enforcement de foreign keys precisa executar o PRAGMA como primeira operação. Se o seu código inicializa o banco via um helper, adicione o PRAGMA ali — e documente isso, porque vai escapar da atenção de alguém da equipe eventualmente.
O custo de não fazer isso é silencioso: você acumula registros órfãos sem nenhum erro no log. Descobrir o problema significa uma consulta manual para encontrar referências quebradas, e corrigi-lo significa decidir entre deletar os registros inválidos ou criar os registros pai ausentes. Se o volume de dados corrompidos for grande, a correção vira uma migration delicada em produção.
Um helper de inicialização que sempre executa o PRAGMA antes de qualquer outra operação é o investimento mais simples possível contra esse problema. A query PRAGMA foreign_keys = ON leva menos de um milissegundo. O tempo para debugar dados inválidos em produção é consideravelmente maior.
Leia também
- Durable Objects em produção: o que a conta vai parecer e os limites que surpreendem
- Cloudflare Workers em produção: o que muda depois do hello world
- KV em produção: os padrões que funcionam e os que enganam no início
- Workers: limites de CPU e memória — o que a documentação não explica direito
- Cloudflare D1: o banco SQLite no edge — e por que 'edge' não significa o que parece
- Consultas lentas em D1: como diagnosticar e otimizar