31 de dez. de 2025·8 min de leitura

Transmitir grandes respostas de API: compressão, exportações e limites

Aprenda a transmitir grandes respostas de API com gzip/brotli, exportações seguras e limites sensatos para que relatórios grandes baixem sem travar seu app.

Transmitir grandes respostas de API: compressão, exportações e limites

Por que grandes respostas de API travam apps

Grandes respostas costumam falhar de um jeito familiar: o relatório funciona no seu laptop com um banco pequeno, depois começa a dar timeout, travar ou retornar um arquivo parcial em produção. Os dados são maiores, a rede é mais lenta e o servidor está lidando com tráfego real ao mesmo tempo.

A maioria dos apps quebra porque constrói a resposta inteira em memória antes de enviar qualquer coisa. Um endpoint “baixar relatório” consulta muitas linhas, formata em JSON ou CSV, armazena o resultado inteiro num buffer e só então escreve para o cliente. Isso provoca picos de memória, ativa garbage collection e deixa todo o resto mais lento.

Os mesmos pontos de falha aparecem repetidamente:

  • Picos de memória por fazer buffer do payload inteiro (às vezes mais de uma vez durante retries)
  • Timeouts no servidor de aplicação, proxy/reverse load balancer ou cliente porque a resposta demora demais
  • Clientes lentos que leem os dados devagar, mantendo conexões abertas e ocupando workers
  • Retries que dobram a carga quando o sistema já está no limite
  • Respostas JSON enormes, caras de serializar e caras de parsear no cliente

O objetivo não é “criar o maior arquivo possível.” É entregar grandes respostas de forma confiável para que uma exportação pesada não degrade o app para todo mundo.

Você muitas vezes resolve isso sem reescrever a feature. A maioria das equipes obtém estabilidade combinando três ideias: comprimir quando ajuda, transmitir dados em pedaços em vez de colocar tudo em buffer, e aplicar limites (tamanho, tempo e linhas) que batem com o que os usuários realmente precisam.

Se você herdou um app gerado por IA onde exportações travam ou a autenticação quebra no meio do download, isso geralmente é corrigível com mudanças direcionadas (buffering, queries sem limites, backpressure ausente). FixMyMess (fixmymess.ai) frequentemente vê essas exportações que “funcionam no dev e quebram em prod” e ajuda a transformá-las em downloads seguros para produção.

Compressão, streaming e limites: o que cada um resolve

Grandes respostas costumam falhar por um de três motivos: o payload é grande demais para mover rapidamente, grande demais para manter em memória, ou caro demais para gerar. Compressão, streaming e limites atacam cada problema diferente.

A compressão deixa o payload menor na rede. O servidor envia menos bytes e o cliente baixa mais rápido. Isso ajuda mais quando o conteúdo é texto (JSON, CSV). Ajuda menos quando os dados já estão comprimidos (imagens, PDFs, ZIPs). A compressão também não conserta o erro básico de construir uma string de 200 MB em memória primeiro.

Streaming muda como você entrega a resposta. Em vez de construir toda a exportação e só então enviar, você envia em pequenos pedaços conforme os produz. Essa é a principal ferramenta quando você precisa enviar milhões de linhas sem esgotar a RAM. Streaming mantém a memória estável, mas não torna a resposta automaticamente menor nem mais barata de computar.

Exportações são um caso especial. Enviar um JSON de 200 MB não é o mesmo que oferecer um download. Um download pode ser transmitido como resposta tipo arquivo (CSV/JSONL) e processado conforme chega. Uma resposta JSON gigantesca costuma forçar clientes a parsear tudo de uma vez, o que pode travar a UI ou derrubar apps móveis.

Limites são a rede de segurança. Eles impedem que pedidos no pior caso derrubem seu app quando alguém seleciona “todo o tempo” e “todos os clientes.” Bons limites geralmente incluem número máximo de linhas ou bytes, tempo máximo por requisição e rate limits em endpoints de exportação. Muitas equipes também definem padrões sensatos, como um intervalo de datas limitado, e exigem um job assíncrono/background para relatórios muito grandes.

Equipes que consertam exportações geradas por IA normalmente precisam dos três: compressão para velocidade, streaming para segurança de memória e limites para que um pedido não prejudique todo mundo.

Escolhendo gzip ou brotli sem adivinhação

Compressão significa que o servidor empacota a resposta em menos bytes antes de enviar. O cliente descompacta automaticamente. Para payloads JSON grandes e exportações, isso pode ser a diferença entre um download rápido e uma requisição que dá timeout.

Gzip é a opção antiga e amplamente suportada. Brotli é mais novo e frequentemente reduz um pouco mais, especialmente em JSON e HTML. Ambos funcionam melhor em texto. Nenhum ajuda muito se sua resposta já está comprimida.

Como cliente e servidor decidem

Os clientes dizem ao servidor o que conseguem decodificar usando o header Accept-Encoding (por exemplo: br, gzip). O servidor deve escolher uma dessas codificações e definir Content-Encoding na resposta. Se o header estiver ausente, envie a resposta normal sem compressão.

Uma regra prática: escolha a melhor codificação que o cliente aceita, com um fallback seguro.

  • Se br é aceito, use Brotli para respostas de texto.
  • Caso contrário, se gzip é aceito, use gzip.
  • Se nenhum for aceito, envie bytes sem compressão.

Quando gzip é mais seguro e quando faz sentido usar brotli

Escolha gzip por padrão quando precisar de máxima compatibilidade (clientes antigos, proxies incomuns ou ambientes mistos). Escolha Brotli quando a maior parte do tráfego for navegadores modernos ou seus próprios clientes controlados, e você se importar em economizar um pouco mais de banda.

Mantenha em mente a troca de CPU. Respostas menores podem custar mais trabalho do servidor. Brotli costuma usar mais CPU que gzip em configurações similares. Se seu servidor já está ocupado gerando relatórios, a compressão pode empurrá-lo além do limite. Uma abordagem comum é gzip para a maioria do JSON de API, Brotli para endpoints voltados ao navegador, e níveis de compressão menores para downloads muito grandes.

Também pule compressão para formatos já comprimidos (arquivos ZIP, PDFs, PNG/JPEG, muitos tipos de áudio/vídeo). Você desperdiça CPU e às vezes faz o arquivo ficar maior.

Se você está herdando um backend gerado por IA, um bom caminho “estabilizar rápido” é gzip para respostas de texto mais limites claros de tamanho. Adicione Brotli apenas onde você puder provar que ajuda.

Como adicionar compressão com segurança

Compressão é frequentemente o ganho mais rápido, mas pode criar bugs confusos se os headers estiverem errados ou os dados forem comprimidos duas vezes.

Comece com uma regra simples: comprima somente quando ajuda. Se a resposta é pequena, a compressão pode desperdiçar CPU e adicionar latência. Um limiar prático é cerca de 1–2 KB para JSON e 4–8 KB para CSV ou texto simples. Abaixo disso, envie sem compressão.

Compressão funciona melhor para conteúdo baseado em texto como JSON, CSV, HTML e logs. Normalmente faz pouco por imagens, PDFs ou arquivos já comprimidos (ZIP). Para esses, pule compressão.

Defina headers para que navegadores, proxies e caches se comportem:

  • Content-Encoding: gzip ou br para dizer ao cliente o que você usou
  • Vary: Accept-Encoding para que caches não misturem versões comprimidas e não comprimidas
  • Um Content-Type correto (por exemplo application/json ou text/csv) para que clientes parseiem corretamente
  • Se você fizer streaming, evite definir Content-Length que não pode garantir

Evite dupla compressão. Isso acontece quando seu app comprime respostas e um reverse proxy ou middleware também comprime. Escolha um único lugar para fazer isso e confirme checando headers de resposta e fazendo um teste rápido para garantir que os bytes batem com Content-Encoding.

Teste como um mini benchmark: compare tamanho da resposta, tempo total e CPU antes e depois. Teste também redes lentas e “cancelar no meio do download”, já que compressão mal configurada frequentemente aparece como downloads quebrados ou requisições penduradas.

Streaming de exportações para não estourar memória

A forma mais segura de lidar com exportações grandes é nunca construir o arquivo inteiro em memória. Gere uma linha (ou um pequeno lote) e envie ao cliente imediatamente. Feito certo, o servidor faz trabalho constante e o download cresce com o tempo.

Para exportações, formatos tipo arquivo costumam ser mais fáceis que “um enorme array JSON” porque você pode escrever linha a linha. CSV funciona bem para fluxo de trabalho com planilhas. NDJSON (um objeto JSON por linha) funciona bem para processamento por máquina e dados em estilo log.

Quando você transmite grandes respostas, conexões lentas importam. Se um usuário baixa por uma rede móvel fraca, o servidor não deve colocar a exportação inteira em buffer enquanto espera para enviar. Use escrita que respeite backpressure (a maioria dos frameworks web suporta isso) para que você só produza dados tão rápido quanto podem ser entregues.

Downloads longos também precisam de timeouts amigáveis. Mantenha a conexão viva com saída periódica e ajuste timeouts de servidor/proxy altos o suficiente para o tamanho esperado do relatório. Se você tem um reverse proxy na frente, confirme que ele permite respostas de longa duração ou cortará a exportação pela metade.

Streaming muda o tratamento de erros. Depois que você começa a enviar o arquivo, não dá para mudar para uma resposta JSON bonita de erro. Planeje isso antes de liberar:

  • Valide entradas e permissões antes de enviar o primeiro byte.
  • Escreva um cabeçalho cedo (colunas CSV, ou uma linha de metadados para NDJSON).
  • Se algo falhar no meio do stream, registre, pare de forma limpa e deixe claro que o arquivo está incompleto.

Um bug comum que “funciona no dev” é construir arrays com centenas de milhares de linhas. Mudar para exportações streaming normalmente remove o pico de memória imediatamente e mantém o app responsivo enquanto o download acontece.

Passo a passo: implementar um download streaming seguro

Desembarace arquitetura espaguete
Refatoramos código gerado por IA para que exportações continuem rápidas conforme os dados crescem.

Trate um download como um pipe ao vivo, não um grande objeto que você monta em memória e retorna no final.

Comece escolhendo o formato de exportação com base em como as pessoas usam. CSV é ótimo para planilhas. NDJSON é melhor quando outro sistema vai ler. Um bundle zip pode ajudar quando você envia vários arquivos, mas não use ZIP só para esconder problemas de desempenho.

Em seguida, torne o trabalho incremental. Em vez de uma query enorme, leia linhas em páginas (ou via cursor) e itere até não haver mais dados. Seu app deve segurar apenas uma pequena fatia por vez.

Uma sequência simples que evita a maioria das falhas “relatório matou o servidor”:

  • Defina headers logo no começo (tipo + nome do arquivo para download) e inicie a resposta.
  • Busque dados em páginas e converta cada página em linhas de saída.
  • Escreva chunks e dê flush com frequência (não monte uma string gigante).
  • Comprima on-the-fly quando ajudar (gzip streaming é amplamente suportado).
  • Pare o trabalho quando o cliente desconectar ou o usuário cancelar.

Compressão é o bônus, não a base. Ela reduz banda, mas streaming é o que mantém a memória estável. Para CSV e NDJSON, gzip normalmente traz grande vantagem, desde que você comprima enquanto escreve em vez de depois de gerar todo o arquivo.

Valide com um conjunto de dados realisticamente grande. Um teste de 1.000 linhas pode parecer perfeito enquanto uma exportação de 5 milhões de linhas esgota memória, dá timeout ou produz um arquivo truncado.

Exemplo: uma exportação “Transações Mensais” falha em produção porque carrega todas as linhas e então junta em uma única string CSV. Trocar para loop paginado mais escritas em chunks corrige sem mudar o que o usuário recebe.

Aplique limites de tamanho e tempo que os usuários aceitem

Se você quer grandes respostas sem crashes aleatórios, precisa de limites que protejam o servidor e ainda pareçam justos para os usuários. A pegada é tornar limites previsíveis, visíveis no comportamento e acompanhados de um próximo passo óbvio.

Comece com dois limites rígidos: linhas máximas e bytes máximos. Caps por linha impedem queries lentas de rodarem para sempre. Caps por bytes previnem respostas “bem-sucedidas” que sobrecarregam proxies ou buffers. Quando uma exportação atingir um cap, retorne uma mensagem clara explicando o que aconteceu e o que mudar (por exemplo, “Exportação limitada a 100.000 linhas. Reduza o intervalo de datas ou adicione um filtro.”).

Coloque proteções na própria query para que o banco faça menos trabalho. Guardrails comuns incluem um intervalo de datas padrão (31 ou 90 dias), exigir ao menos um filtro restritivo para relatórios “todos os clientes” e um tamanho máximo de página mesmo se o cliente pedir mais. Se você permitir ordenação ou filtragem, mantenha uma allow-list e garanta que o banco dá conta.

Limits de tempo devem existir em múltiplas camadas: timeout de statement no banco, timeout de requisição no servidor e um deadline na aplicação para gerar exportações. Quando você interrompe o trabalho, faça isso de forma limpa. Retorne um erro específico que diga aos usuários como ter sucesso na próxima vez, não um 500 genérico.

Rate limiting é a outra metade de limites “vivos”. Um usuário baixando repetidamente um relatório enorme não deve prejudicar todo mundo. Faça throttling em endpoints caros por usuário e por organização, e considere limites distintos para requisições interativas versus exportações.

Finalmente, registre requisições perto dos limites (linhas, bytes, tempo, filtros usados) e alerte quando elas se acumulam. Se muitos usuários atingem um cap de 90 dias, isso é um sinal para adicionar um relatório resumido ou uma opção de export assíncrona.

Verificações de segurança para exportações e grandes respostas

Resolva timeouts em grandes respostas
Consertamos lógica quebrada, retries e consultas lentas que fazem grandes respostas falharem.

Grandes exportações falham de duas formas: travam o app ou vazam dados silenciosamente. Trate exportações como uma feature separada com regras de segurança próprias.

Comece pela autorização. Um bug comum é um endpoint de export que checa “usuário está logado?” mas esquece “ele pode ver essas linhas?” Reutilize as mesmas checagens de permissão do relatório na tela e aplique-as no servidor antes de escrever qualquer dado.

Exportações CSV têm um risco especial: CSV injection. Se um campo controlado pelo usuário começa com caracteres como =, +, - ou @, abrir o arquivo em uma planilha pode executar uma fórmula. A correção é simples: escape ou prefixe valores arriscados (por exemplo, adicione um apóstrofo à frente) para células exportadas vindas de usuários.

Falhas em exportações também podem vazar segredos. Quando um job dá timeout, é tentador logar a query completa, headers ou body da requisição. Isso pode expor chaves de API, tokens de auth ou dados pessoais nos logs. Prefira um ID interno de export mais um código de erro curto, e mantenha valores sensíveis fora das stack traces.

Filtros flexíveis são outra armadilha. “Ordene por qualquer coluna” ou “filtre com uma string de query crua” pode virar SQL injection se você montar SQL por concatenação de strings. Use queries parametrizadas e allow-lists para campos ordenáveis e filtráveis.

Proteja também exportações de longa execução contra abuso. Um pequeno conjunto de guardrails ajuda muito:

  • Re-valide o token do usuário (ou sessão) quando a exportação começar, não apenas quando foi enfileirada
  • Aplique throttling por usuário/workspace
  • Coloque um cap rígido em linhas ou tempo e retorne uma mensagem clara quando o limite é atingido
  • Registre quem exportou o quê e quando para auditoria

Essas checagens são fáceis de perder quando o foco é “fazer o download”. O objetivo é um download que seja confiável e seguro em produção.

Erros comuns que causam downloads quebrados

Downloads quebrados geralmente acontecem porque o servidor tenta ser “prestativo” no lugar errado. Um teste rápido com dados pequenos parece OK, então um relatório real em produção trava o app, dá timeout ou retorna um arquivo que não abre.

Uma armadilha fácil é compressão em todo lugar. Comprimir uma resposta JSON de 2 KB pode custar mais CPU do que economiza, especialmente sob carga. Compressão brilha quando respostas são grandes e repetitivas (exportações, listas longas, logs). Para respostas pequenas, pule ou imponha um tamanho mínimo.

Outro erro comum é construir toda a exportação em memória antes de enviar. Parece mais simples criar uma string ou buffer grande, mas escala mal. Um CSV de 200 MB pode virar muito maior em memória durante o formatação, e alguns usuários fazendo isso ao mesmo tempo podem derrubar o processo.

Outros erros que aparecem com frequência:

  • Chamar algo de “streaming” enquanto ainda gera o CSV inteiro primeiro e depois escreve
  • Fazer streaming de JSON de forma que produza JSON inválido (colchetes faltando, vírgulas finais, objetos parciais)
  • Ignorar timeouts do cliente, proxy reverso ou load balancer (a exportação roda, mas a conexão já morreu)
  • Testar apenas numa rede local rápida com dados pequenos e mandar pra produção sem testar rede lenta ou tamanho real
  • Bater em limites (tamanho/tempo) sem mensagem clara, então usuários só veem um download falhado

Streaming JSON merece cuidado especial. Se você precisa de JSON estrito, transmita um array bem-formado e gerencie as vírgulas com cuidado. Se os clientes aceitarem, escolha um formato feito para streaming como JSON Lines/NDJSON.

Quando limites são atingidos, diga ao usuário o que aconteceu e o que fazer a seguir (reduzir filtros, intervalo de datas menor ou pedir uma exportação assíncrona).

Um exemplo realista: consertando uma exportação que falha

Um fundador clica em “Relatório mensal de vendas” e espera. A aba do navegador gira, o app fica lento para todo mundo e, depois de um minuto, o download falha. No servidor, o endpoint de relatório estava montando todo o CSV em memória antes de enviar qualquer coisa. Um mês grande (ou algumas colunas extras) empurra a memória para além do limite e o processo reinicia.

A correção não foi “aumentar o servidor”. Foi mudar como a exportação é produzida e entregue para lidar com grandes conjuntos de dados de forma previsível.

O que mudou:

  • O servidor escreve linhas CSV conforme as lê do banco, em vez de acumulá-las numa grande string.
  • Compressão gzip é habilitada para o download, então o arquivo fica menor na rede.
  • Um cap rígido é adicionado (por exemplo, 31 dias por vez) com uma mensagem clara de erro se o usuário pedir mais.
  • Um timeout e limite de linhas são aplicados para que uma requisição não possa monopolizar o sistema.

A experiência do usuário melhora na hora. O download começa em um ou dois segundos porque o servidor pode enviar headers e os primeiros bytes imediatamente. O arquivo frequentemente termina mais rápido porque está menor, e as falhas diminuem porque o servidor não tenta guardar tudo em memória. Se o usuário precisa de um intervalo maior, a UI pode orientar a rodar múltiplas exportações.

Para a equipe, o maior ganho é estabilidade. Memória fica estável, picos de CPU são menores e tickets como “o relatório travou o app” desaparecem. Esse é o tipo de trabalho que a FixMyMess faz quando protótipos gerados por IA quebram em produção: mover exportações para streaming, adicionar compressão segura e aplicar limites para que uma exportação única não derrube o app.

Checklist rápido antes de enviar

Torne exportações seguras em produção
Transforme sua funcionalidade de exportação gerada por IA numa transferência streaming segura para produção.

Teste o pior caso, não o caminho feliz. Escolha o maior relatório que seus usuários podem pedir realisticamente e rode de ponta a ponta do jeito que eles farão (mesmos filtros, mesmos papéis e, idealmente, um dispositivo real). É aí que downloads “funcionam na minha máquina” normalmente desmoronam.

Checklist:

  • Rode a maior exportação e confirme que ela termina com sucesso (sem 500s, sem arquivos parciais, sem “erro de rede” depois de alguns minutos).
  • Observe memória do servidor durante a exportação. Ela deve ficar basicamente estável. Uma subida lenta e contínua geralmente indica buffering em vez de streaming.
  • Faça limites visíveis onde os usuários escolhem o relatório: intervalo máximo de datas, caps de linhas e qualquer timeout.
  • Verifique autorização com papéis reais (admin, usuário padrão, papéis restritos). Confirme que não dá para exportar dados que você não pode ver.
  • Cheque logs após uma execução grande: tamanho da resposta, se compressão foi usada, tempo gasto gerando e se a requisição bateu algum limite.

Se você herdou uma exportação gerada por IA que continua travando, a correção mais rápida costuma ser uma auditoria curta para achar onde o buffering acontece, depois aplicar streaming e limites rígidos.

Próximos passos se seu app já está travando

Se o app trava quando usuários rodam relatórios grandes, trate como incidente: pare o sangramento primeiro e depois melhore a experiência. Afinar compressão antes de ter guardrails costuma desperdiçar tempo.

Uma ordem sensata de trabalho:

  • Adicione limites rígidos (max linhas, max bytes, max tempo) e retorne um erro claro quando forem atingidos.
  • Troque exportações para streaming para que o servidor nunca carregue o arquivo inteiro em memória.
  • Ajuste compressão depois que o básico estiver estável, e só onde for útil.

Depois que os limites estiverem no lugar, você consegue suportar downloads grandes sem derrubar o processo. A mudança-chave é evitar buffering: não monte o JSON/CSV completo num array ou string e não logue payloads completos em erros.

Faça um plano de testes do pior caso antes de tocar em produção:

  • O maior relatório que usuários realmente executam (ou a maior tabela em prod)
  • Uma rede cliente lenta (throttle) durante o download
  • Duas ou três exportações concorrentes de usuários diferentes
  • Um download cancelado na metade
  • Uma requisição que atinge o limite (verifique a mensagem e que o servidor continua saudável)

Se sua base de código foi gerada por ferramentas como Lovable, Bolt, v0, Cursor ou Replit, esses bugs costumam se esconder em alguns lugares: wrappers de auth que retryam para sempre, tratamento de erro que despeja respostas inteiras nos logs, e utilitários helper que chamam coisas como toString() ou json() cedo demais (forçando buffering completo).

Uma remediação rápida normalmente parece com: diagnóstico (encontrar onde a memória sobe e onde o buffering acontece), correções direcionadas (limites + streaming + erros mais seguros), verificação (testes de carga e checagens de integridade da exportação) e preparação para deploy (timeouts, dimensionamento de workers e monitoramento). Se quiser uma segunda opinião, FixMyMess em fixmymess.ai pode rodar uma auditoria de código gratuita para localizar pontos de crash em exportações, lacunas de segurança e problemas de performance, e ajudar a entregar uma correção funcional em 48–72 horas.