DEV Community

Messias Freitas
Messias Freitas

Posted on

Como servir os 68 milhões de CNPJs da Receita com ~10ms de latência em Go

Todo dev brasileiro que já precisou consultar CNPJ conhece o dilema: ou você
usa uma API que faz proxy da Receita (3 a 10 segundos por consulta, quando não
cai), ou baixa o dump de dados abertos e monta a própria base — e descobre que
"baixar um CSV" era a parte fácil.

Eu montei a própria base. Este post é o diário honesto do que funcionou, do
que quebrou e dos números reais — 217 milhões de linhas servidas em ~10ms de
p50 dentro do datacenter, num Postgres de 1 vCPU.

A arquitetura em uma frase

Não consulte a Receita em tempo real. Ingira o dump mensal e sirva da sua
infra.
O resto é decorrência.

Receita (dump mensal, ~6GB zip) ──▶ ingestão Go (COPY) ──▶ Postgres ──▶ API (chi)
CGU (CEIS/CNEP, zip diário)     ──▶ job diário          ──┘
Enter fullscreen mode Exit fullscreen mode

O dump da Receita: as pegadinhas que ninguém documenta

O layout oficial existe, mas o que quebra parser de verdade é o que está fora dele:

  1. Encoding latin1 (ISO-8859-1) — acento vira lixo se você ler como UTF-8. Em Go: charmap.ISO8859_1.NewDecoder() num transform.Reader streaming.
  2. Decimal com vírgula ("1000000,00") e datas YYYYMMDD onde 0 e 00000000 significam nulo.
  3. CNPJ quebrado em 3 colunas (básico 8 + ordem 4 + DV 2). A chave de junção entre empresas, estabelecimentos e sócios é o básico — errar isso custa um dia.
  4. As partições 0–9 não se alinham entre arquivos. O estabelecimento da partição 3 pode ser de uma empresa da partição 7. Foreign key rígida entre as tabelas = COPY quebrando no meio da carga. A solução: sem FK; a integridade vem da fonte.
  5. Bytes NUL (0x00) no meio dos dados. O Postgres rejeita NUL em text. Um strings.ReplaceAll(s, "\x00", "") no parser economizou três recargas.
  6. Desde jan/2026 o repositório é um Nextcloud do SERPRO+ com WebDAV público — dá pra listar meses com PROPFIND e baixar com o token do share como usuário. Adeus, scraping.

COPY ou morte

A diferença entre INSERT em lote e o protocolo COPY não é incremental — é
outra categoria. Com pgx.CopyFrom e lotes de 50k:

  • 28,1 milhões de empresas em 1m28s (~320k linhas/s) num MacBook
  • A base completa (68,6M empresas + 71,9M estabelecimentos + 27,8M sócios + 49M de registros do Simples) entra em cerca de uma hora numa VM modesta

O truque é não materializar nada: latin1→UTF-8→parse→COPY, tudo streaming,
memória constante.

O índice que paga o aluguel

A feature mais pedida por quem faz compliance é a busca reversa: de um nome
de pessoa para todas as empresas em que ela é sócia. Com 27,8 milhões de sócios:

CREATE INDEX idx_socios_nome_trgm ON socios USING gin (nome_socio gin_trgm_ops);
Enter fullscreen mode Exit fullscreen mode

Lição aprendida: ORDER BY similarity(...) avalia todos os matches antes
do LIMIT. Com threshold 0.4 num nome comum ("JOSE..."), timeout; com 0.6,
~1s. O operador % respeita o set_limit() da sessão — alinhe o threshold do
índice com o do filtro ou o plano de query mente pra você.

Cruzar sanções com o QSA (o pulo do gato)

O CEIS/CNEP (empresas e pessoas punidas, CGU) publica CPF completo. O QSA
da Receita publica CPF mascarado: ***XXXXXX** — os 6 dígitos do meio.

Então o match sócio-sancionado é: substring(cpf_mascarado, 4, 6) =
substring(cpf_completo, 4, 6)
+ similaridade trigram de nome ≥ 0.5. Não é
prova — é sinal forte, e em compliance sinal forte com explicação vale mais que
caixa-preta. Numa base real: empresa limpa, sócio proibido de contratar com o
poder público até 2030. É esse cruzamento que consulta nenhuma te dá de graça.

Números de produção (Cloud Run + Cloud SQL 1 vCPU)

Operação Latência
Ficha completa (3 queries + JOINs de tradução) ~8ms no datacenter, ~134ms na minha casa
Checagem de sanções (empresa + sócios) 14–34ms
Score com sinais ~96ms
Busca reversa em 27,8M de nomes ~1s

Custo total da infra: ~R$ 300/mês. O concorrente que faz proxy da Receita
responde em 3.000–10.000ms e cai junto com ela.

Coisas que quebraram e viraram conhecimento

  • O Google Frontend reserva /healthz nos domínios *.run.app — o request nem chega no container. Use /health.
  • Cloud Run com scale-to-zero: primeiro request paga ~6s de cold start (pool do Postgres). Aceitável para o meu caso; min-instances=1 resolve por ~R$ 30/mês.
  • gcloud sql connect não funciona se seu ISP te dá IPv6. cloud-sql-proxy autentica via IAM e ignora o problema.
  • Payment Links do Stripe suportam assinatura de R$ 0 sem cartão (payment_method_collection: if_required) — free tier com identidade real, zero código de checkout.

O resultado

A API está no ar em fides.api.br — tem playground sem
cadastro na landing e um free tier de 50 consultas/mês que pede só email. Se
você só precisa de razão social e endereço, a BrasilAPI é grátis e resolve;
eu mantenho uma comparação honesta que
inclui quando não usar o meu produto.

O código do parser de latin1 e a estratégia de COPY descritos aqui são
genéricos — servem pra qualquer dataset dos dados abertos brasileiros.

Dúvidas sobre o pipeline? Comenta que eu respondo — a parte de infra rendeu
mais história do que coube aqui.

Top comments (0)