Blog

Limite de tamanho de payload: quando a requisição legítima passa a ser recusada

O parceiro integrou em janeiro, rodou nove meses sem um único erro e na terça-feira passou a receber recusa em uma requisição a cada cem. O corpo era o mesmo formato de sempre, o token era válido, o endereço não mudou e o erro chegava antes de qualquer log da aplicação: o serviço nunca viu aquela requisição. O que mudou não foi o cliente e não foi o servidor, foi a distribuição do tamanho dos pedidos, que cresceu o suficiente para encostar em um teto que ninguém escolheu conscientemente e que está declarado em quatro lugares diferentes do caminho. Este artigo mostra por que o limite efetivo é o menor de uma cadeia e não o que está no seu código, por que o erro aparece sem corpo e sem rastro e o que isso faz com o suporte, qual é a diferença entre o limite que protege memória e o limite que protege tempo e por que confundir os dois cria uma brecha, por que aumentar o número é a correção errada na maioria dos casos e qual é a certa, como transformar um teto invisível em contrato explícito que o cliente consegue respeitar antes de enviar, e quais cinco verificações separam um pedido abusivo de um pedido legítimo que simplesmente cresceu.

2026-09-21 / Arquitetura / 18 min

01

O limite que vale é o menor da cadeia, e ele não está no seu código

A primeira reação de quem recebe o relato é abrir o código do serviço e procurar onde o tamanho máximo do corpo está configurado. Encontra-se um valor, ele parece generoso, e a conclusão imediata é que o problema deve estar em outro lugar. A conclusão está certa pelo motivo errado: o problema realmente está em outro lugar, porque o valor encontrado no código é apenas um dos quatro ou cinco tetos que uma requisição precisa atravessar, e o que decide o destino dela é o menor deles, não o último.

Uma requisição típica em produção passa por uma rede de distribuição de conteúdo, um balanceador gerenciado, um servidor de borda que faz terminação de conexão segura, um proxy reverso interno e finalmente o processo da aplicação. Cada uma dessas camadas tem um limite próprio, cada uma tem um padrão diferente, e nenhuma delas consulta as outras. O padrão de um servidor de borda popular é um megabyte, o de um gateway gerenciado costuma ser dez, o de um framework de aplicação frequentemente é cem kilobytes, e o da função sem servidor que alguém colocou no meio do caminho no ano passado pode ser seis megabytes com codificação obrigatória em base 64, o que derruba a capacidade útil para pouco mais de quatro.

A consequência prática é que a resposta para a pergunta qual é o tamanho máximo que o meu serviço aceita não pode ser lida em nenhum arquivo de configuração isolado. Ela precisa ser medida atravessando o caminho inteiro, com uma requisição real, do lado de fora. Essa medição leva quinze minutos e é a única forma honesta de responder a um parceiro que pergunta quanto ele pode enviar.

Camada do caminhoPadrão típico quando ninguém configurouFormato do erro que ela devolveAparece no log da aplicação
Rede de distribuição de conteúdoEntre 100 MB e sem limite, conforme o planoPágina de erro genérica do provedorNão, a requisição nunca sai da borda
Balanceador gerenciado da nuvem1 MB a 10 MB conforme o tipoCódigo 413 sem corpo ou com corpo padrãoSó na métrica do balanceador, não na aplicação
Servidor de borda ou proxy reverso1 MB na configuração padrão mais comumPágina HTML de erro, não JSONNo log do proxy, não no da aplicação
Framework ou middleware de corpo100 KB em vários ecossistemasExceção tratável, formato controlado por vocêSim, e é o único ponto onde isso é verdade
Função sem servidor no meio do caminho6 MB já contando a codificação de transporteErro de invocação, frequentemente 502Não, e o rastro fica no provedor

A coluna mais importante é a última. Em quatro das cinco camadas a requisição recusada não gera nenhuma linha no log da aplicação, o que significa que o painel de erros do time permanece limpo enquanto o parceiro acumula falhas. Esse descompasso é o que faz o incidente durar dias: o suporte pede o identificador de rastreamento da requisição, o parceiro não tem um porque nenhuma resposta o devolveu, e o time procura no lugar onde o evento nunca foi registrado.

Requisicao de 2,4 MB atravessando a cadeia:

  cliente
    |  POST /v1/lotes  (2,4 MB)
    v
  +------------------------+
  | CDN          limite 100 MB   | -> passa
  +------------------------+
    |
    v
  +------------------------+
  | balanceador  limite 10 MB    | -> passa
  +------------------------+
    |
    v
  +------------------------+
  | proxy reverso limite 1 MB    | -> RECUSA AQUI
  +------------------------+       413, HTML, sem rastreio
    |                              log da aplicacao: vazio
    X  (a requisicao morre)
  +------------------------+
  | aplicacao    limite 8 MB     | -> nunca executa
  +------------------------+

Limite efetivo = min(100, 10, 1, 8) = 1 MB
O valor no codigo da aplicacao (8 MB) e irrelevante.

02

Por que o erro chega sem corpo, sem rastreio e sem explicação

Existe uma razão técnica para a recusa por tamanho ser a mais pobre em informação de toda a família de erros de cliente. Quando um serviço recusa uma requisição por autenticação inválida, por exemplo, ele já leu o cabeçalho, já identificou o chamador, já tem um identificador de rastreamento e pode devolver um corpo estruturado explicando o que falhou. Na recusa por tamanho nada disso aconteceu, porque a decisão precisa ser tomada antes de ler o corpo, justamente para não gastar o recurso que o limite existe para proteger.

Há um segundo efeito, menos conhecido e mais desagradável, que explica por que às vezes o cliente vê uma conexão fechada abruptamente em vez de um código de erro limpo. Quando o servidor decide recusar no meio do envio, ele responde e quer encerrar, mas o cliente ainda está escrevendo os megabytes restantes no soquete. O servidor então fecha a conexão com os dados pendentes, e o cliente, que estava no meio de uma escrita, recebe um erro de conexão reiniciada pelo par antes de conseguir ler a resposta que já tinha chegado. O parceiro reporta erro de rede, o servidor registra 413, e os dois têm razão.

Esse é o motivo pelo qual a correção mais valiosa desse incidente raramente é mexer no número. É fazer a recusa carregar informação. Um erro que diz quanto foi enviado, quanto é permitido, qual camada recusou e o que fazer em seguida transforma um chamado de suporte de três dias em uma correção de dez minutos do lado do cliente, e isso vale mesmo quando o limite permanece exatamente onde estava.

// Middleware de recusa informativa. A decisao acontece antes de ler o corpo,
// olhando apenas o cabecalho anunciado, e a resposta carrega o que o cliente
// precisa para se corrigir sozinho.

const LIMITE_BYTES = 1 * 1024 * 1024; // teto efetivo medido na cadeia, nao o do framework

export function limitePayload(req, res, next) {
  const anunciado = Number(req.headers['content-length']);

  // Requisicao sem tamanho anunciado usa transferencia em partes: o teto
  // precisa ser aplicado durante a leitura, nao antes dela.
  if (!Number.isFinite(anunciado)) return limitarDuranteLeitura(req, res, next);

  if (anunciado > LIMITE_BYTES) {
    // Responder sem consumir o corpo. O cliente pode estar no meio do envio,
    // entao pedimos o encerramento explicito da conexao para evitar que ele
    // receba um erro de soquete em vez desta resposta.
    res.set('Connection', 'close');
    return res.status(413).json({
      erro: 'payload_acima_do_limite',
      limite_bytes: LIMITE_BYTES,
      recebido_bytes: anunciado,
      camada: 'aplicacao',
      // O ponto que resolve o chamado: dizer o que fazer, nao so o que falhou.
      acao: 'Divida o lote em partes de no maximo 500 itens ou use POST /v1/lotes/upload para envio em duas etapas.',
      documentacao: 'https://exemplo.dev/docs/limites',
    });
  }

  return next();
}

// Para envio em partes o tamanho real so e conhecido ao longo da leitura.
// Contamos os bytes e abortamos assim que o teto e ultrapassado, sem
// acumular o restante em memoria.
function limitarDuranteLeitura(req, res, next) {
  let lidos = 0;

  req.on('data', (parte) => {
    lidos += parte.length;
    if (lidos <= LIMITE_BYTES) return;

    res.set('Connection', 'close');
    res.status(413).json({
      erro: 'payload_acima_do_limite',
      limite_bytes: LIMITE_BYTES,
      recebido_bytes: lidos,
      camada: 'aplicacao',
      acao: 'Anuncie content-length ou reduza o tamanho do envio em partes.',
    });

    // Interrompe a leitura: sem isso o processo continua recebendo bytes que
    // ja decidimos descartar, que e exatamente o custo que o limite evita.
    req.destroy();
  });

  req.on('end', () => {
    if (!res.headersSent) next();
  });
}

Duas linhas desse trecho costumam ser esquecidas em implementações caseiras e ambas têm consequência operacional direta. A primeira é o encerramento explícito da conexão, sem o qual o cliente frequentemente perde a resposta que o serviço acabou de enviar. A segunda é a destruição do fluxo de entrada quando o limite é ultrapassado durante a leitura: sem ela o processo continua recebendo e descartando bytes até o fim do envio, gastando exatamente a banda e a memória que o limite deveria ter economizado.

03

Dois limites diferentes com o mesmo nome: memória e tempo

Quando alguém pergunta por que existe um limite de tamanho, a resposta padrão é proteção contra abuso. A resposta está incompleta e a incompletude cria uma brecha real. Existem dois motivos distintos para limitar tamanho, eles protegem recursos diferentes, e um limite calibrado para um deles não protege contra o outro.

O primeiro motivo é memória. Um corpo de requisição que é lido inteiro para dentro do processo antes de ser processado ocupa memória proporcional ao tamanho, multiplicada pelo número de requisições simultâneas e novamente por um fator de expansão que quase ninguém contabiliza. Um JSON de dez megabytes vira uma estrutura de objetos que ocupa entre três e dez vezes isso na memória do processo, dependendo da linguagem e do formato dos dados. Com cinquenta requisições simultâneas desse tamanho, o cálculo que parecia confortável vira encerramento do processo por falta de memória.

O segundo motivo é tempo de ocupação. Uma requisição grande enviada lentamente prende um trabalhador do servidor durante todo o envio, e é esse o vetor da classe de ataque em que o agressor anuncia um corpo pequeno e o envia byte a byte, sem nunca ultrapassar limite algum de tamanho. Nenhum teto de bytes protege contra isso, porque o tamanho total é legítimo: o que é abusivo é a taxa. A defesa é outra, chama-se tempo mínimo de recebimento ou taxa mínima de entrada, e é uma configuração separada que vive em outro lugar da pilha.

RiscoRecurso protegidoConfiguração corretaO que NÃO protege contra ele
Corpo grande demais carregado em memóriaMemória do processoTeto de bytes aplicado antes da desserializaçãoTimeout de requisição, que dispara tarde demais
Envio deliberadamente lento de corpo pequenoTrabalhadores e conexões livresTaxa mínima de recebimento e timeout de leituraLimite de tamanho, porque o total é legítimo
Expansão durante a desserializaçãoMemória e processadorLimite de profundidade e de número de nós do documentoLimite de bytes, que mede o comprimido e não o expandido
Corpo comprimido que expande muitas vezesMemória do processoTeto do tamanho descomprimido, verificado durante a expansãoTeto de bytes, que vê apenas o tamanho na rede
Muitas requisições no limite ao mesmo tempoMemória agregada do serviçoOrçamento de bytes em voo, não apenas por requisiçãoLimite por requisição isolado, que ignora a soma

As duas últimas linhas são as que mais frequentemente faltam em serviços que já se consideram protegidos. Um corpo comprimido de cem kilobytes que expande para um gigabyte passa por qualquer teto de bytes medido na rede, e a defesa precisa contar os bytes descomprimidos ao longo da expansão, abortando no meio dela. E um limite de dez megabytes por requisição, com duzentas requisições simultâneas permitidas, é na prática uma autorização para dois gigabytes de corpos em voo, que é um número que ninguém teria aprovado se tivesse sido escrito assim.

// Verificacao de corpo comprimido com teto sobre o tamanho descomprimido.
// O teto de bytes na rede nao enxerga expansao: 100 KB comprimidos podem
// virar 1 GB em memoria, e a checagem precisa acontecer durante a expansao.

import { createGunzip } from 'node:zlib';

const LIMITE_DESCOMPRIMIDO = 8 * 1024 * 1024;
const RAZAO_MAXIMA = 50; // expansao acima disso e sinal de payload construido

export async function lerCorpoComprimido(req) {
  const comprimidoAnunciado = Number(req.headers['content-length']) || 0;
  let descomprimido = 0;
  const partes = [];

  const expansor = req.pipe(createGunzip());

  try {
    // Iterar sobre o fluxo expandido permite decidir a cada bloco, antes de
    // ter o documento inteiro em memoria. E o unico ponto onde a checagem
    // ainda e barata.
    for await (const parte of expansor) {
      descomprimido += parte.length;

      // Dois tetos independentes: o absoluto protege a memoria do processo,
      // o de razao detecta o payload desenhado para expandir.
      if (descomprimido > LIMITE_DESCOMPRIMIDO) {
        throw new ErroPayload('descomprimido_acima_do_limite', {
          limite_bytes: LIMITE_DESCOMPRIMIDO,
          recebido_bytes: descomprimido,
        });
      }

      if (comprimidoAnunciado > 0 && descomprimido / comprimidoAnunciado > RAZAO_MAXIMA) {
        throw new ErroPayload('razao_de_expansao_suspeita', {
          razao: Math.round(descomprimido / comprimidoAnunciado),
          razao_maxima: RAZAO_MAXIMA,
        });
      }

      partes.push(parte);
    }
  } finally {
    // Encerra a expansao e a leitura da requisicao mesmo quando abortamos no
    // meio: sem isso o processo continua recebendo bytes ja descartados.
    expansor.destroy();
    req.destroy();
  }

  return Buffer.concat(partes);
}

class ErroPayload extends Error {
  constructor(codigo, detalhes) {
    super(codigo);
    this.codigo = codigo;
    this.detalhes = detalhes;
    this.status = 413;
  }
}

04

Aumentar o número é a correção errada na maioria dos casos

A pressão do incidente empurra para a solução de um caractere: trocar um por dez na configuração e encerrar o chamado. Essa mudança funciona, custa nada e é a resposta certa em exatamente um cenário, que é quando o limite atual foi herdado de um padrão que ninguém escolheu e o novo valor foi calculado contra a memória disponível. Em todos os outros cenários ela apenas move a data do próximo incidente e piora a exposição enquanto isso.

O sinal que distingue os casos é a forma da distribuição de tamanhos. Se a recusa atinge uma fração pequena e estável das requisições e o percentil noventa e nove está logo abaixo do teto, o crescimento é orgânico e o limite realmente ficou apertado. Se a recusa atinge pouquíssimas requisições e o tamanho delas é uma ordem de grandeza maior que o percentil noventa e nove, não é crescimento: é um cliente específico fazendo algo diferente, e aumentar o limite vai transformar uma recusa barata em um processamento caro que ninguém dimensionou.

  1. Meça a distribuição real de tamanhos por cliente nos últimos trinta dias, não a média agregada, e olhe o percentil cinquenta, o noventa e nove e o máximo separadamente.
  2. Identifique se as requisições recusadas são a cauda natural da distribuição ou um grupo isolado muito acima dela, porque as duas formas pedem correções opostas.
  3. Calcule o teto que a memória suporta: memória disponível por instância dividida pelo fator de expansão do formato, dividida pelo número de requisições simultâneas permitidas.
  4. Compare o teto suportado com o teto desejado, e se o desejado for maior, a correção não é configuração, é mudar o padrão de envio do cliente.
  5. Ofereça um caminho alternativo explícito para os pedidos grandes legítimos, como paginação no envio ou envio em duas etapas, antes de subir qualquer número.
  6. Aplique o mesmo valor em todas as camadas da cadeia, porque um teto maior na aplicação com o proxy inalterado não muda absolutamente nada.

O terceiro item é o que costuma encerrar a discussão em times que estavam prestes a subir o limite para cinquenta megabytes. Uma instância com dois gigabytes de memória, um fator de expansão de cinco para JSON e cem requisições simultâneas permitidas suporta um teto teórico de quatro megabytes por corpo, e isso sem contar nenhuma outra alocação do processo. O número que o time queria configurar estava uma ordem de grandeza acima do que a máquina aguenta, e a única razão pela qual isso não tinha quebrado antes é que ninguém tinha enviado corpos daquele tamanho ainda.

// Teto sustentavel de corpo por requisicao, derivado da memoria da instancia
// em vez de escolhido por intuicao. O numero que sai daqui costuma ser bem
// menor do que o que o time pretendia configurar.

/**
 * @param {number} memoriaMb        memoria disponivel por instancia
 * @param {number} reservaMb        memoria que o processo usa sem nenhuma requisicao
 * @param {number} simultaneas      requisicoes concorrentes permitidas
 * @param {number} fatorExpansao    quantas vezes o corpo cresce ao virar objeto
 * @param {number} margemSeguranca  fracao da memoria que fica livre de proposito
 */
export function tetoSustentavelDeCorpo({
  memoriaMb = 2048,
  reservaMb = 400,
  simultaneas = 100,
  fatorExpansao = 5,
  margemSeguranca = 0.3,
}) {
  const disponivelMb = (memoriaMb - reservaMb) * (1 - margemSeguranca);

  // Cada requisicao em voo ocupa o corpo cru mais a estrutura expandida.
  // Ignorar o fator de expansao e o erro que faz o calculo dar cinco vezes
  // mais do que a maquina realmente aguenta.
  const porRequisicaoMb = disponivelMb / simultaneas;
  const tetoMb = porRequisicaoMb / (1 + fatorExpansao);

  return {
    tetoMb: Number(tetoMb.toFixed(2)),
    tetoBytes: Math.floor(tetoMb * 1024 * 1024),
    // Se o teto desejado for maior que este, a correcao nao e configuracao:
    // e reduzir a concorrencia, aumentar a memoria ou mudar o padrao de envio.
    observacao: `Com ${simultaneas} requisicoes simultaneas e expansao de ${fatorExpansao}x, o teto seguro e ${tetoMb.toFixed(2)} MB por corpo.`,
  };
}

// 2048 MB, reserva 400, margem 30%, 100 simultaneas, expansao 5x
// -> 1153 MB uteis / 100 = 11,53 MB por requisicao / 6 = 1,92 MB de corpo.
// O time queria configurar 50 MB.

05

Transformar o teto invisível em contrato que o cliente consegue respeitar

A propriedade que torna esse incidente recorrente é que o limite só é comunicado no momento da falha, e de forma pobre. Nenhum cliente consegue respeitar um contrato que ele descobre por tentativa e erro em produção. A correção estrutural é publicar o limite de três formas complementares, cada uma atendendo a um momento diferente do ciclo de vida da integração.

  • Na documentação e no esquema da API, com o valor em bytes e a regra de contagem explícita: se o que conta é o corpo cru ou o descomprimido, e se cabeçalhos entram na conta.
  • Em um endereço de descoberta que devolve os limites vigentes, para que o cliente possa validar antes de enviar e para que uma mudança de teto não exija um novo ciclo de deploy do parceiro.
  • Na própria resposta de erro, com o limite, o tamanho recebido e a ação recomendada, porque é ali que a informação chega a quem está com o problema na mão.
  • Em um cabeçalho de resposta presente também nas requisições bem-sucedidas, indicando quanto da margem aquele pedido consumiu, o que dá ao cliente um sinal de aproximação antes da primeira recusa.
  • Em um caminho alternativo documentado para os casos legítimos que excedem o teto, sem o qual a única saída do parceiro é dividir o pedido de forma que pode não ser correta no domínio dele.

O quarto item é o de melhor relação entre esforço e retorno e o menos implementado dos cinco. Um cabeçalho que informa em toda resposta bem-sucedida a fração do limite consumida transforma o teto de um penhasco em uma rampa: o cliente que está em oitenta por cento sabe disso meses antes de bater, pode alertar o próprio time e pode ajustar o padrão de envio sem nenhum incidente no meio. O custo de produzir esse cabeçalho é um número que o servidor já tem na mão.

// Endereco de descoberta e cabecalho de proximidade. O objetivo e que o
// cliente nunca descubra o limite por tentativa e erro em producao.

const LIMITES = {
  corpo_bytes: 1_048_576,
  corpo_descomprimido_bytes: 8_388_608,
  itens_por_lote: 500,
  conta: 'corpo cru apos descompressao, cabecalhos nao entram',
  alternativa_para_envios_maiores: '/v1/lotes/upload',
};

// 1) Descoberta: o cliente consulta e se adapta sem depender de deploy nosso.
export function rotaDeLimites(_req, res) {
  res.set('Cache-Control', 'public, max-age=3600');
  res.json({ limites: LIMITES, versao: '2026-09-21' });
}

// 2) Proximidade: toda resposta bem-sucedida diz quanto da margem foi usada.
// O cliente em 80% descobre meses antes de bater no teto, e nao no incidente.
export function anunciarConsumo(req, res, next) {
  const tamanho = Number(req.headers['content-length']) || 0;
  if (tamanho > 0) {
    const fracao = tamanho / LIMITES.corpo_bytes;
    res.set('X-Payload-Limit', String(LIMITES.corpo_bytes));
    res.set('X-Payload-Size', String(tamanho));
    res.set('X-Payload-Usage', fracao.toFixed(3));

    // Aviso formal a partir de 80%: da ao time do cliente um gancho para
    // alertar sem precisar interpretar um numero solto.
    if (fracao >= 0.8) {
      res.set(
        'Warning',
        `199 - "payload em ${Math.round(fracao * 100)}% do limite; veja ${LIMITES.alternativa_para_envios_maiores}"`,
      );
    }
  }

  next();
}

O endereço de descoberta tem um benefício de segunda ordem que costuma decidir a discussão: ele permite reduzir um limite sem quebrar ninguém. Com o valor publicado e consultado, o serviço pode anunciar o teto novo com semanas de antecedência, medir quantos clientes ainda enviam acima dele e só então aplicar a mudança. Sem isso, qualquer redução de limite é uma quebra silenciosa que aparece como incidente do lado do parceiro.

06

Cinco verificações que separam crescimento legítimo de abuso

A decisão operacional que o time precisa tomar durante o incidente é uma só: este pedido grande é legítimo e merece acomodação, ou é anômalo e a recusa está certa? Responder por intuição leva a dois erros caros em direções opostas, que são acomodar um padrão abusivo e rejeitar um cliente importante que apenas cresceu. As cinco verificações a seguir respondem com dados em poucos minutos.

  1. Compare o tamanho recusado com o percentil noventa e nove histórico daquele mesmo cliente: dentro da mesma ordem de grandeza indica crescimento, uma ordem acima indica mudança de comportamento.
  2. Verifique se o crescimento é no número de itens do lote ou no tamanho médio por item, porque o primeiro é resolvido com paginação e o segundo frequentemente indica campo novo ou dado duplicado no payload.
  3. Procure repetição interna no corpo recusado: chaves repetidas, o mesmo objeto aninhado várias vezes ou campos preenchidos com valores idênticos indicam defeito de montagem do lado do cliente, não necessidade real.
  4. Confirme se o mesmo cliente passou a enviar sem compressão, porque uma mudança de biblioteca que desliga a compressão multiplica o tamanho na rede sem que nada tenha mudado no dado.
  5. Cheque a correlação com uma data de deploy do parceiro: um salto em degrau no gráfico de tamanhos coincidindo com uma data única é mudança de código, e um crescimento suave ao longo de semanas é volume de negócio.
Padrão observadoLeituraAção recomendadaPrazo
Crescimento suave, percentil 99 encostando no tetoVolume de negócio real do clienteRecalcular o teto pela memória e subir em toda a cadeiaDias, com janela planejada
Salto em degrau em uma data únicaMudança de código do parceiroAcionar o parceiro com o dado antes de mexer no limiteHoras, é reversível do lado dele
Corpo com repetição interna altaDefeito de montagem do payloadDevolver o diagnóstico ao cliente e manter a recusaImediato, a recusa está correta
Mesmo dado, tamanho maior, compressão ausenteRegressão de configuração do clienteExigir compressão no contrato e sinalizar no erroImediato, correção é de uma linha
Poucos pedidos, várias ordens acima do normalAbuso ou teste automatizado fora de ambienteManter recusa, aplicar limite por cliente e registrarImediato, sem acomodação

A terceira linha merece destaque porque é a mais comum das cinco e a que mais frequentemente recebe a correção errada. Um payload com repetição interna alta quase sempre vem de um laço que acumula sem limpar, de um campo de contexto que é anexado a cada item em vez de uma vez por lote, ou de uma serialização que repete o objeto pai dentro de cada filho. Aumentar o limite nesse caso é pagar com memória do seu serviço por um defeito no cliente, e o crescimento não para no valor novo: ele volta a encostar no teto na próxima vez que o laço rodar mais vezes.

A instrumentação que sustenta essas cinco verificações é modesta: um histograma de tamanho de corpo com rótulo por cliente e por rota, um contador de recusas com o mesmo rótulo, e a razão entre tamanho comprimido e descomprimido. Com esses três sinais, a pergunta que hoje leva três dias de troca de mensagens com o parceiro passa a ser respondida por um painel em dois minutos, e a resposta vem com o dado que convence os dois lados.

FAQ

Perguntas frequentes

Qual é o valor certo para o limite de tamanho de corpo em uma API pública?

Não existe um valor universal, mas existe um método que produz o valor certo para um caso concreto, e ele tem quatro passos que podem ser executados em uma tarde. O primeiro é derivar o teto que a infraestrutura sustenta, que é a memória disponível por instância menos a reserva do processo, aplicada a margem de segurança, dividida pelo número de requisições simultâneas permitidas e novamente pelo fator de expansão do formato, que fica entre três e dez para JSON dependendo da linguagem e da proporção de números e strings nos dados. Esse cálculo quase sempre devolve um número menor do que o time esperava, e é ele que define o máximo aceitável do ponto de vista de sobrevivência do serviço. O segundo passo é medir a distribuição real de tamanhos dos clientes existentes nos últimos trinta dias, separando por cliente e olhando o percentil noventa e nove de cada um, porque a mediana agregada esconde exatamente o cliente que vai quebrar. O terceiro é escolher o teto como um múltiplo confortável do maior percentil noventa e nove legítimo, tipicamente entre duas e três vezes, desde que esse valor caiba abaixo do teto que a infraestrutura sustenta. Se não couber, o resultado do exercício não é um limite maior: é a constatação de que aquele caso de uso precisa de um caminho de envio diferente, seja em duas etapas com um endereço de upload, seja paginado, seja assíncrono com um identificador de trabalho. O quarto passo é aplicar o valor escolhido em todas as camadas do caminho e verificar de fora que ele é realmente o efetivo, porque um teto novo na aplicação com o proxy inalterado não muda absolutamente nada e produz um incidente de segunda rodada com o time convencido de que já tinha corrigido. Como ordem de grandeza para calibrar a intuição, APIs de integração empresarial costumam ficar entre um e dez megabytes, e valores acima disso quase sempre indicam que o caso de uso é de transferência de arquivo disfarçada de chamada de API.

Como oferecer um caminho para pedidos legítimos que realmente não cabem no limite?

Há três padrões consolidados e a escolha entre eles depende de uma pergunta de domínio, não de infraestrutura: o pedido grande precisa ser atômico? Se a resposta for não, que é o caso mais comum, a solução é paginação no envio com uma chave de agrupamento. O cliente divide o lote em partes de tamanho previsível, envia cada uma com o mesmo identificador de lote e uma marcação de última parte, e o servidor consolida ao receber o encerramento. Esse padrão preserva a semântica de conjunto, permite reenvio de uma parte isolada sem repetir tudo e mantém cada requisição dentro do teto normal, o que significa que nada na cadeia precisa ser afrouxado. A chave de idempotência por parte é o que torna o reenvio seguro. Se a resposta for sim, e o pedido precisa ser atômico, o padrão correto é o envio em duas etapas: uma primeira chamada pequena solicita um endereço de escrita temporário e devolve um identificador, o cliente escreve o conteúdo diretamente no armazenamento de objetos usando aquele endereço, e uma segunda chamada pequena informa que o conteúdo está pronto e dispara o processamento. Essa forma tem três vantagens que compensam a complexidade extra: o corpo grande nunca atravessa a sua cadeia de serviço, o armazenamento cuida de retomada e integridade sem código seu, e o limite da API permanece baixo para todos os outros clientes. O terceiro padrão é o processamento assíncrono com identificador de trabalho, apropriado quando o pedido é grande porque representa uma operação demorada e não porque carrega muitos dados: o cliente envia a descrição compacta do trabalho, recebe um identificador imediatamente e consulta o resultado depois. O erro comum nos três casos é não documentar o caminho alternativo junto com o limite, o que deixa o parceiro com a impressão de que a única saída é insistir no pedido grande até alguém do outro lado subir o número.

O limite deve ser o mesmo para todos os clientes ou pode variar por contrato?

Pode e frequentemente deve variar, mas a variação precisa ser implementada de uma forma específica para não virar uma fonte de incidentes pior do que o limite único. O princípio é que existem dois tetos com naturezas diferentes e apenas um deles pode ser negociado. O teto de infraestrutura, derivado da memória e da concorrência, é um limite físico do serviço: nenhum contrato comercial pode exceder esse valor, porque o que está do outro lado não é uma política e sim o encerramento do processo por falta de memória. O teto de política, que é o valor aplicado a cada cliente, vive abaixo do teto de infraestrutura e pode perfeitamente ser diferenciado por plano, por integração ou por rota. A implementação precisa observar três cuidados. O primeiro é que o teto por cliente deve ser resolvido a partir de uma configuração consultável e cacheada, nunca de uma lista embutida no código, porque senão cada ajuste comercial vira um ciclo de deploy e a diferenciação acaba abandonada na prática. O segundo é que o limite da camada de borda precisa ser o teto de infraestrutura e não o teto do cliente mais generoso, com a diferenciação aplicada na camada de aplicação, que é a única que sabe quem é o chamador: tentar diferenciar no proxy exige identificar o cliente antes de ler o corpo, o que é frágil e costuma quebrar quando a autenticação muda. O terceiro é que o valor vigente para aquele chamador precisa aparecer no endereço de descoberta e no cabeçalho de proximidade, porque um limite diferenciado que o cliente não consegue consultar é indistinguível de um limite instável do ponto de vista dele. Um efeito colateral positivo dessa arquitetura é que ela dá ao time um mecanismo de contenção granular durante incidentes: reduzir temporariamente o teto de um único cliente abusivo é uma operação de configuração, não de deploy, e não afeta ninguém mais.

Limite de tamanho é contrato, e contrato que só aparece no erro não é contrato

A recusa por tamanho é o erro mais pobre em informação de toda a família de erros de cliente, e é assim por uma razão técnica legítima: a decisão precisa ser tomada antes de ler o corpo. Isso não obriga a resposta a ser inútil. Medir o limite efetivo atravessando a cadeia inteira, derivar o teto sustentável da memória em vez de escolhê-lo por intuição, separar o limite que protege memória do que protege tempo, publicar o valor em um endereço de descoberta e anunciar a proximidade em toda resposta bem-sucedida transformam um penhasco invisível em uma rampa que o cliente enxerga meses antes de bater. Posso levantar o limite efetivo real do seu caminho, calcular o teto que a sua infraestrutura sustenta, desenhar o caminho alternativo para os pedidos grandes legítimos e instrumentar os sinais que separam crescimento de abuso antes do próximo chamado.