Validação por Marca — OOM em produção e limite de tamanho de arquivo
Documento de contexto para discussão com o time. Branch:
emplacamento/hotfix/validacao-por-marca-out-of-rangeData: 2026-08-14
1. Resumo executivo
A geração de arquivo da Validação por Marca (POST /api/emplacamento/validacao-marca/gerar)
quebra em produção com grandes volumes de dados. São dois problemas distintos,
descobertos em sequência:
-
Estouro de memória (OOM) — a função na Vercel era morta por falta de RAM (
instance was killed because it ran out of available memory, 2048 MB). → Já corrigido nesta branch (streaming via cursor). -
Arquivo grande demais para o Storage — depois de corrigir a memória, o arquivo gerado ultrapassa o limite de tamanho do bucket do Supabase Storage (
The object exceeded the maximum allowed size, limite de 100 MB no bucket / 50 MiB global). → Em aberto — precisa de decisão do time (seção 5).
2. Como o fluxo funciona hoje
Usuário (tela Validação da Marca)
│ seleciona período, montadoras, área, tipo (padrão/detalhado), formato (CSV/XLSX)
▼
POST /api/emplacamento/validacao-marca/gerar
│ 1. valida permissão
│ 2. cria registro em brand_validations (status = processing)
│ 3. consulta o DW (dw_emplacamento_mart.emplacamento_detalhe + joins)
│ 4. monta o arquivo (CSV ou XLSX) em memória
│ 5. sobe o arquivo no bucket "validacao-marca" (Supabase Storage)
│ 6. marca status = completed e devolve um signedUrl
▼
Navegador baixa o arquivo direto pelo signedUrl do Storage
Pontos relevantes de arquitetura:
- A geração é síncrona dentro da própria route handler (não há fila/worker; é o equivalente moderno do antigo EventBridge/Lambda do sistema legado).
- O DW é um Postgres separado acessado via
postgres.js(não é o Supabase do app). Passa por Supavisor em transaction mode (por issoprepare: false). - O download é feito direto pelo navegador contra o Storage, usando um signedUrl de 1h. A aplicação não fica no meio do download.
3. Problema 1 — Estouro de memória (OOM) — CORRIGIDO
Causa raiz
O código carregava todo o resultado da query em memória e ainda o duplicava várias vezes:
queryDetalhe(...)→ array completo de objetos JS (milhões de linhas — o item mais pesado, por causa do overhead de objeto/string por campo);rows.map(buildRow)→ segundo array completo;[header, ...bodyRows].map().join()→ um array de strings de linha e uma string única gigante concatenada;Buffer.from(csvContent)→ mais uma cópia.
Pico de memória ≈ 4 a 5× o tamanho do dataset. Com o volume de produção, estourava os 2048 MB da função e a Vercel matava a instância.
Correção aplicada (nesta branch)
Streaming via cursor do Postgres — nunca segura todas as linhas de uma vez.
validacao-marca-service.ts: novo métodostreamDetalhe(), um async generator que usaquery.cursor(2000)(entrega no máximo 2000 linhas por vez).gerar/route.ts:- CSV: montado em chunks de
Buffer— cada lote do cursor vira bytes e é descartado em seguida. Sem teto de linhas. - XLSX: monta a planilha linha a linha a partir do cursor (elimina a
duplicação
rows+bodyRows). Como o SheetJS materializa a planilha inteira em memória (não tem escrita em streaming na versão community), foi imposto um teto de 200.000 linhas com mensagem pedindo para usar CSV ou reduzir período/filtros. export const maxDuration = 300para exportações longas usarem o teto de 5 min da função.
- CSV: montado em chunks de
Detalhe técnico importante (para o time)
O cursor é seguro sob o Supavisor em transaction mode: o .cursor() do
postgres.js roda dentro de uma transação, e em transaction pooling a transação
inteira fica "presa" (pinned) a uma única conexão física — então todos os
FETCH sucessivos batem no mesmo backend. O idle_timeout não fecha a conexão
durante a iteração (ela está em uso, não idle).
Status: ✅ implementado, com typecheck + lint + format limpos.
4. Problema 2 — Arquivo maior que o limite do Storage — EM ABERTO
Depois de resolver a memória, o arquivo é gerado com sucesso, mas no passo de upload o Supabase Storage rejeita:
The object exceeded the maximum allowed size
Causa
O arquivo bruto de milhões de linhas passa dos limites de tamanho:
- Bucket
validacao-marca:file_size_limit = 104857600(100 MB) — definido na migration20260801000001_brand_validations.sql. - Limite global do Storage:
50 MiBem desenvolvimento (config.toml); em produção é a configuração do dashboard da Supabase.
O limite efetivo é o menor dos dois. Um CSV detalhado de período longo com
todas as montadoras pode facilmente passar de 100 MB (estimativa: milhões de
linhas × ~150 bytes/linha → centenas de MB).
Restrição do fluxo atual
Como o download é feito direto pelo navegador contra o Storage (signedUrl), o arquivo precisa, hoje, existir descompactado no bucket. Isso amarra as opções.
5. Soluções propostas (para decidir com o time)
Opção A — Gzip + rota de download própria ⭐ (recomendada)
Comprime o arquivo antes de subir e serve o download através de uma rota nossa.
- No
gerar:buffer → gzip → uploadcomo.csv.gz/.xlsx.gz. - No download: em vez de redirecionar pro signedUrl, uma rota nossa faz
stream do objeto do Storage de volta ao navegador com os headers:
Content-Encoding: gzip→ o navegador descompacta sozinho;Content-Disposition: attachment; filename=...→ salva como.csv/.xlsx.
- O usuário baixa um arquivo normal, sem perceber a compressão.
Prós
- CSV comprime ~85–90% (ex.: 400 MB → ~40 MB). Fica bem abaixo de qualquer limite.
- Não depende de mudar nada no dashboard da Supabase.
- Funciona para praticamente qualquer tamanho.
- Downloads ficam muito mais rápidos (menos banda).
- A rota de download faz stream (não segura o arquivo em memória).
Contras
- Mexe em mais lugares:
gerar(gzip), rota dedownload(proxy com headers) e no client (passar a baixar pela rota em vez do signedUrl direto). - Precisa validar o comportamento de download com
Content-Encoding: gzipnos navegadores usados.
Opção B — Aumentar os limites do Storage
Sobe o file_size_limit do bucket e o limite global.
- Migration:
update storage.buckets set file_size_limit = ...(ex.: 1 GB). config.toml:file_size_limit = "1GiB"(dev).- Dashboard de produção: aumentar o Upload file size limit na Supabase (manual — precisa ser feito por alguém com acesso ao projeto).
Prós
- Mínimo de código.
- Mantém o fluxo atual (download direto por signedUrl).
Contras
- Depende de mudança manual no dashboard de produção (não dá pra fazer só no repositório).
- Uploads e downloads de centenas de MB via upload padrão são lentos e podem falhar — para arquivos realmente grandes o recomendado é resumable upload (TUS), que é mais complexo.
- O usuário baixa arquivos enormes (banda/tempo).
- Não resolve o problema de fundo, só empurra o teto.
Opção C — Limitar o tamanho da exportação
Impor um teto de linhas também no CSV (como já existe no XLSX) e barrar com mensagem pedindo para reduzir período/filtros.
Prós
- Simples e previsível; evita o erro por completo.
Contras
- Não entrega os dados grandes que o usuário precisa — só impede o erro.
- Ruim se as exportações grandes forem um caso de uso legítimo (aparentemente são).
Opção D — Geração assíncrona (evolução futura, fora do hotfix)
Tirar a geração de dentro da request e mandar para uma fila/worker (o sistema legado usava EventBridge/Lambda). A tela já escuta status via Realtime, então a UX de "processando → pronto" já está pronta para isso.
Prós
- Elimina de vez os limites de tempo/memória da request.
- Caminho natural para volumes muito grandes.
Contras
- Bem mais trabalho; não é um hotfix. Fica como recomendação de médio prazo.
6. Recomendação
- Hotfix imediato: manter a correção de memória (Problema 1, já feita) e adotar a Opção A (gzip + rota de download) para o Problema 2 — é a única que resolve de verdade sem depender de configuração manual em produção e sem degradar a experiência de download.
- Médio prazo: avaliar a Opção D (geração assíncrona) se o volume continuar crescendo.
- Combinar com um teto de sanidade (uma variação leve da Opção C) só para proteger contra exportações absurdas continua sendo saudável.
7. Perguntas para o time
- Qual o tamanho real esperado dessas exportações (ordem de grandeza)?
- Há restrição para mudar o Upload file size limit no dashboard de produção?
- Tem apetite para a geração assíncrona (Opção D) agora ou deixamos como evolução?
- Existe um teto de negócio aceitável para o número de linhas por exportação?
8. Arquivos envolvidos
| Arquivo | Papel |
|---|---|
apps/web/app/api/emplacamento/validacao-marca/gerar/route.ts | Gera o arquivo e sobe no Storage |
apps/web/app/api/emplacamento/validacao-marca/[id]/download/route.ts | Devolve signedUrl para re-download |
apps/web/app/[locale]/emplacamento/home/[account]/validacao-da-marca/_lib/server/validacao-marca-service.ts | Query no DW + upload/URL do Storage |
apps/web/app/[locale]/emplacamento/home/[account]/validacao-da-marca/_components/validacao-marca-view.tsx | UI + download no client |
apps/web/supabase/migrations/20260801000001_brand_validations.sql | Cria bucket (limite 100 MB) e tabela |
apps/web/supabase/config.toml | Limite global do Storage (50 MiB em dev) |