Sirvo 55 milhões de páginas em uma VPS de R$ 200/mês: o setup completo
Olá pessoal!
Seguindo a sequência de posts que venho fazendo nos últimos dias. Hoje falo sobre minha infra atual para rodar o cnpjaberto.com.br. No último (e penúltimo) post algumas pessoas vieram me perguntar qual era o custo de infra disso, e a respota é, bastante baixo! Mas isso só acontece devido a milhares de otimizações, truques que aprendi ao longo dos anos e outros que a IA (ela mesmo) acabou ensinando. Desfrutem!
Resumo da operação: 1 servidor, 4 containers Docker, 0 CDN, 0 load balancer, 0 serviço gerenciado. Hetzner CCX23 — 4 vCPU AMD dedicado, 16 GB RAM, 160 GB NVMe, 20 TB de tráfego. €29,74/mês líquido, uns R$ 200 dependendo do dólar. Acabo sempre arredondando para cima.
A máquina roda apenas o projeto cnpjaberto.com.br: um site para consultar o CNPJ de forma gratuita, os dados vem diretamente Receita Federal, hoje, o site possui ~70 milhões de estabelecimentos, ~67 milhões de empresas e ~27 milhões de sócios. 80 GB de dados ativos no Postgres, e cada estabelecimento ativo tem sua própria página com sitemap apontando, daí o "55 milhões de páginas" do título dos posts anteriores que postei aqui.
1. O hardware
Hetzner CCX23 (linha CCX é AMD dedicado, não a CX compartilhada, diferença concreta em paralelismo do Postgres):
- 4 vCPU AMD dedicado
- 16 GB RAM
- 160 GB NVMe local
- 20 TB de saída/mês (rede 1 Gbps)
Datacenter em Ashburn, US. Latência pro usuário brasileiro fica ~120ms. Isso complica um pouco, mas um servidor em SP ficaria muito mais caro e não quero gastar, ao menos agora.
2. Quatro containers, um host
- postgres # 16-alpine, 80 GB de dados
- redis # 7-alpine, 2GB maxmemory, allkeys-lru
- backend # FastAPI, 4 workers uvicorn
- frontend # Next.js 15, SSR sob demanda
Algumas pessoas comentaram em trocar o postgres pelo duckdb. Acabei não alterando pois o sistema já funciona bem com postgres, mas parece uma dica muito válida. Outro ponto: hoje mantenho o postgresql pois quero uma solução SAAS. O duckdb me parece uma solução de data lake ou analytics, me corrijam se estiver errado.
3 (gargalo). Postgres 80GB de dados, 16GB de RAM
Os parâmetros que importam do Postgres:
- shared_buffers 3 GB # buffer interno (~20% da RAM)
- effective_cache_size 9 GB # estimativa do que SO + PG cacheia
- work_mem 16 MB # baixo de propósito (sort/hash por op)
- maintenance_work_mem 1 GB # alto pra CREATE INDEX / VACUUM rápidos
- random_page_cost 1.1 # NVMe ≈ leitura sequencial
- max_wal_size 4 GB
- jit off # planning alto, ganho marginal em OLTP
A parte crítica é shared_buffers + effective_cache_size + (work_mem × max_connections). Com 100 conexões, posso estourar 1.6 GB só em ordenação se o work_mem estiver alto. Por isso 16 MB e não os 64+ MB que tutoriais de internet
recomendam.
- random_page_cost=1.1 foi a virada de chave pra fazer o planner usar índices em vez de scan sequencial em quase tudo, NVMe não tem latência rotacional (go SSDs!), a heurística default (4.0) está mentindo pra ele.
Os índices que mais carregam o trabalho:
- B-tree em cnpj em estabelecimentos (lookup direto)
- GIN com pg_trgm em razão social, fantasia e nome de sócio (busca fuzzy — escrevi um post sobre isso aqui também)
- B-tree em cnae_principal, municipio, uf (filtros do panorama estatístico)
4. As seis camadas de cache
A maioria do tráfego não chega a tocar o Postgres em runtime. Sim.
| Camada | Onde | TTL |
|---|---|---|
| Lookup Tables | RAM do processo Python | até restart (para sempre) |
| Detalhes CNPJ | Redis | 1h |
| Resultados de busca | Redis | 1h |
| Panorama (cnpjaberto.com.br/panorama) | Snapshot postgres | 30d |
| Sitemap XML | unstable_cache | 30d |
| Bolsa/FII | RAM do Next | 7d |
O redis usa allkeys-lru, ou seja, uma key mais recente invalida outras keys. Isso ajuda a controlar o espaço/tamanho do Redis.
5. Next.js sem SSG, SSR sob demanda
Já escrevi aqui um post inteiro sobre isso, então só o resumo: gerar 55 milhões de páginas estáticas em build time é inviável (tempo, disco, e o delta mensal da RFB destruiria qualquer estratégia de rebuild). A solução é SSR sob demanda unstable_cache por rota. Primeiro hit em página fria custa caro; segundo hit no mesmo CNPJ vem de cache em milissegundos.
6. Onde o sistema vai bater a parede
Sim, eu sei que isso vai acontecer.
Disco (160 GB): hoje ocupo ~110 GB. A base da RFB cresce ~2-3 GB/mês. Em 18 meses preciso particionar e arquivar histórico, ou subir pra CCX33 (240 GB).
CPU em ingestão: durante a ingestão mensal (2-4h) o Postgres consome os 4 vCPUs. A app fica lenta. Contornável rodando de madrugada, mas é um ponto real de contenção.
Quando bater na parede, o caminho é vertical: CCX33 custa €59/mês (8 vCPU, 32 GB, 240 GB) e provavelmente compra mais 12-18 meses. Sair pra arquitetura distribuída (réplica de leitura, CDN, object storage) só faz sentido depois de outro 10x de tráfego.
Por hoje era isso, obrigado pelo tempo de leitura :)
Menções honrrosas:
Todos que agregaram de alguma forma nos posts anteriores. Em especial ao @Detinho, que recomendou o uso de unstable cache.
Fonte: https://cnpjaberto.com.br/