Blog

Limite de gasto por cliente: cortar o abuso sem punir o uso legítimo

Um cliente de plano básico gastou em três dias o equivalente a onze meses da mensalidade dele. Ninguém invadiu nada: ele plugou o seu endpoint num script próprio que reprocessa o catálogo inteiro a cada hora, e o seu sistema aceitou tudo porque nunca lhe foi ensinado a dizer não. A reação instintiva é baixar um teto de requisições por minuto para todo mundo, e essa é justamente a decisão que transforma um problema de um cliente em degradação de todos os outros. Limite de gasto é um assunto diferente de rate limit, porque o recurso escasso não é vazão, é dinheiro, e dinheiro se acumula ao longo do mês em vez de se renovar a cada segundo. Este artigo mostra como construir esse controle: por que contar requisições é a métrica errada, como usar orçamento em dinheiro com reserva antes da chamada e acerto depois, onde a contabilidade eventualmente consistente quebra e quanto isso custa, como separar abuso de crescimento legítimo antes de bloquear, o que fazer quando o teto é atingido no meio de uma conversa e como avisar o cliente cedo o bastante para ele não descobrir pelo erro.

2026-08-01 / IA Aplicada / 14 min

01

Rate limit protege a infraestrutura, teto de gasto protege a margem

Os dois controles parecem o mesmo mecanismo porque ambos terminam em recusar uma requisição, mas defendem coisas diferentes e falham de formas diferentes. O rate limit existe para que nenhum cliente monopolize a capacidade e derrube o serviço para os demais: a janela é curta, de segundos ou minutos, e o recurso se renova sozinho. O teto de gasto existe para que nenhum cliente consuma mais dinheiro do que a relação comercial com ele suporta: a janela é o ciclo de faturamento, e o recurso consumido não volta. Um cliente pode respeitar perfeitamente cinco requisições por segundo durante trinta dias seguidos e ainda assim gerar um prejuízo, porque cinco requisições por segundo com contexto longo é uma fatura de cinco dígitos.

A consequência prática é que a unidade contada precisa mudar. Rate limit conta eventos, e todo evento vale o mesmo. Teto de gasto precisa contar dinheiro, e nesse mundo duas requisições idênticas na forma podem diferir em cinquenta vezes no custo: uma pergunta curta respondida por um modelo pequeno contra uma conversa longa que arrasta oito trechos de RAG e ainda chama três ferramentas. Se você tenta aproximar dinheiro por contagem de requisições, precisa calibrar o teto pela requisição mais cara possível, o que pune quem só faz perguntas baratas, ou pela média, o que deixa passar exatamente o padrão de uso que gera o prejuízo. Não existe número de requisições que resolva isso, porque a variável que importa não está sendo medida.

AspectoRate limitTeto de gasto
O que protegeCapacidade e latência dos outros clientesMargem do contrato e previsibilidade da fatura
Unidade contadaRequisições ou tokens por janela curtaDinheiro acumulado no ciclo de faturamento
JanelaSegundos a minutos, renova sozinhaDias a um mês, não renova até o fechamento
Custo de errar para menosCliente vê lentidão momentâneaPrejuízo permanente, já foi pago ao provedor
Custo de errar para maisUm cliente degrada os demaisCliente legítimo é bloqueado e cancela o plano
Reação certa ao estourarEnfileirar ou pedir para tentar depoisDegradar para modelo barato ou pedir aprovação

02

Reservar antes de chamar e acertar depois, porque o custo real só existe no fim

O ponto que quebra a implementação ingênua é temporal: você precisa decidir se autoriza a chamada antes de saber quanto ela vai custar. O custo exato só aparece na resposta do provedor, com a contagem real de tokens de entrada, de saída e de cache, e a essa altura o dinheiro já foi gasto. Debitar apenas depois da resposta cria uma janela em que dez requisições paralelas do mesmo cliente passam todas pela verificação enquanto o saldo ainda está intacto, e o estouro só aparece quando as dez retornam. Em atendimento com resposta longa, essa janela dura segundos, tempo suficiente para um script agressivo furar um teto por uma margem grande.

A solução é o mesmo padrão de reserva usado em pagamento com cartão: antes da chamada você estima um teto de custo e reserva esse valor no orçamento do cliente de forma atômica; depois da resposta, substitui a reserva pelo custo real e devolve a diferença. A estimativa não precisa ser exata, precisa ser conservadora: tokens de entrada você já conhece exatamente, porque o prompt está montado na sua mão, e a saída você limita com max tokens, que é justamente o teto que torna a estimativa possível. Se a chamada falhar ou for cancelada, a reserva expira e volta ao saldo. O efeito é que dez requisições paralelas competem pelo mesmo saldo reservado e a décima é recusada antes de gastar, não depois.

// spend-budget.js
// Orcamento de gasto por cliente com reserva atomica antes da chamada
// e acerto pelo custo real depois. Estado no Redis: um hash por cliente
// e ciclo, com o consumido confirmado e o total reservado em aberto.

import { randomUUID } from 'node:crypto';

// Estimativa conservadora: entrada e exata, saida usa o teto de maxTokens.
export const estimateCostCents = ({ model, inputTokens, maxTokens, prices }) => {
  const price = prices[model];
  if (!price) throw new Error(`Sem preco cadastrado para o modelo ${model}`);
  const input = (inputTokens / 1_000_000) * price.inputPerMillionCents;
  const output = (maxTokens / 1_000_000) * price.outputPerMillionCents;
  return Math.ceil(input + output);
};

// Reserva e acerto em Lua para que a leitura do saldo e a escrita da
// reserva sejam um passo so: sem isso, requisicoes concorrentes leem o
// mesmo saldo livre e todas se autorizam.
const RESERVE_SCRIPT = `
  local key = KEYS[1]
  local reservationsKey = KEYS[2]
  local limit = tonumber(ARGV[1])
  local amount = tonumber(ARGV[2])
  local reservationId = ARGV[3]
  local ttl = tonumber(ARGV[4])

  local spent = tonumber(redis.call('HGET', key, 'spent') or '0')
  local reserved = tonumber(redis.call('HGET', key, 'reserved') or '0')

  if spent + reserved + amount > limit then
    return {0, spent, reserved}
  end

  redis.call('HINCRBY', key, 'reserved', amount)
  redis.call('HSET', reservationsKey, reservationId, amount)
  redis.call('EXPIRE', reservationsKey, ttl)
  return {1, spent, reserved + amount}
`;

// O acerto e sempre relativo a reserva registrada: se a reserva ja tiver
// expirado, o custo real ainda precisa entrar no consumido, senao o
// gasto some da contabilidade justamente nas chamadas mais lentas.
const SETTLE_SCRIPT = `
  local key = KEYS[1]
  local reservationsKey = KEYS[2]
  local reservationId = ARGV[1]
  local actual = tonumber(ARGV[2])

  local reserved = tonumber(redis.call('HGET', reservationsKey, reservationId) or '0')
  if reserved > 0 then
    redis.call('HINCRBY', key, 'reserved', -reserved)
    redis.call('HDEL', reservationsKey, reservationId)
  end
  redis.call('HINCRBY', key, 'spent', actual)
  return redis.call('HGET', key, 'spent')
`;

export class SpendBudget {
  constructor(redis, { reservationTtlSeconds = 300 } = {}) {
    this.redis = redis;
    this.reservationTtlSeconds = reservationTtlSeconds;
  }

  #keys(tenantId, cycle) {
    return [`budget:${tenantId}:${cycle}`, `budget:${tenantId}:${cycle}:reservations`];
  }

  async reserve({ tenantId, cycle, limitCents, amountCents }) {
    const reservationId = randomUUID();
    const [ok, spent, reserved] = await this.redis.eval(
      RESERVE_SCRIPT,
      2,
      ...this.#keys(tenantId, cycle),
      String(limitCents),
      String(amountCents),
      reservationId,
      String(this.reservationTtlSeconds),
    );

    if (ok !== 1) {
      return { granted: false, spentCents: spent, reservedCents: reserved };
    }
    return { granted: true, reservationId, spentCents: spent, reservedCents: reserved };
  }

  async settle({ tenantId, cycle, reservationId, actualCents }) {
    const spent = await this.redis.eval(
      SETTLE_SCRIPT,
      2,
      ...this.#keys(tenantId, cycle),
      reservationId,
      String(actualCents),
    );
    return Number(spent);
  }

  // Chamada abortada ou falha antes de consumir token: devolve a reserva
  // sem debitar nada. Sem isso, timeout do provedor come o orcamento do
  // cliente por ate reservationTtlSeconds.
  async release({ tenantId, cycle, reservationId }) {
    return this.settle({ tenantId, cycle, reservationId, actualCents: 0 });
  }
}

Dois detalhes desse código costumam ser descobertos em produção da forma cara. O primeiro é o TTL da reserva: ele precisa ser maior que o timeout máximo da chamada ao modelo, senão a reserva expira enquanto a resposta ainda está em streaming e o acerto tenta descontar algo que já sumiu, inflando o consumido em dobro. O segundo é o acerto quando a reserva já expirou: o custo real precisa entrar no consumido de qualquer forma, porque o dinheiro foi gasto, e o erro comum é tratar reserva ausente como chamada não realizada e simplesmente ignorar o débito, o que faz o gasto desaparecer exatamente nas chamadas mais longas, que são as mais caras.

03

Consistência do contador: onde vale gastar coordenação e onde não vale

A verificação atômica acima resolve a corrida dentro de uma instância de Redis, e é comum o sistema estar distribuído em mais de uma região com réplicas locais para latência. Nesse cenário, insistir em consistência forte global significa que cada mensagem de atendimento paga uma ida e volta entre regiões antes de chamar o modelo, o que adiciona dezenas de milissegundos a um caminho que já é lento. A pergunta correta não é como tornar o contador perfeitamente consistente, é quanto de estouro você aceita em troca de latência, porque essa é a moeda real da negociação.

O desenho que costuma equilibrar bem é o de cotas locais com sincronização periódica: cada região recebe uma fatia do orçamento restante e opera contra ela sem coordenação, devolvendo o não usado e pedindo mais quando a fatia baixa de um piso. O estouro máximo passa a ser limitado e calculável, aproximadamente a soma das fatias em voo, e você escolhe o tamanho da fatia por perfil de cliente: fatia pequena para quem está perto do teto, fatia grande para quem está no começo do ciclo. Perto do limite, o sistema aperta a coordenação automaticamente, e é justamente aí que a exatidão importa. Longe do limite, ninguém precisa pagar por precisão que não muda decisão alguma.

EstratégiaEstouro possívelCusto de latênciaQuando usar
Contador único fortePraticamente zeroUma ida e volta a cada chamada, cross-region se houverTeto rígido contratual ou cliente pré-pago sem crédito
Cotas locais com sincronizaçãoLimitado pela soma das fatias em vooDesprezível no caminho quentePadrão para a maioria dos planos com pós-pago
Débito assíncrono pós-respostaAlto sob concorrênciaNenhumApenas para alerta e relatório, nunca para bloqueio
Fatia adaptativa por proximidadeGrande longe do teto, mínimo perto deleCresce só nos últimos por cento do orçamentoQuando latência e exatidão importam nos dois extremos

04

Separar abuso de crescimento legítimo antes de bloquear qualquer coisa

O teto sozinho não distingue o cliente que descobriu valor no produto e triplicou o uso do cliente que apontou um script mal escrito para o seu endpoint. Os dois aparecem como a mesma curva subindo, e bloquear os dois do mesmo jeito custa caro nos dois sentidos: perder o cliente que ia expandir o contrato é pior do que absorver o prejuízo de um mês. A distinção não está no volume, está na forma do consumo, e existem sinais baratos de calcular que separam bem os dois casos.

  • Diversidade de conteúdo: abuso automatizado costuma repetir o mesmo prompt ou variações triviais dele, então uma taxa alta de acerto no cache semântico com gasto crescente é sinal de laço, não de adoção.
  • Distribuição no tempo: uso humano tem vale noturno e queda no fim de semana, enquanto script tem consumo plano vinte e quatro horas por dia, e o desvio padrão por hora do dia denuncia isso sem nenhum modelo.
  • Razão entre conversas e usuários: crescimento legítimo aumenta os dois juntos, abuso aumenta conversas com o mesmo punhado de identificadores de usuário.
  • Desfecho das conversas: adoção real produz resolução e conversas que terminam, enquanto laço produz conversas abandonadas no meio e uma taxa de transbordo que não muda mesmo com o volume dobrando.
  • Tamanho médio de contexto: salto súbito no comprimento do prompt sem mudança de produto do lado do cliente costuma indicar integração nova despejando documento inteiro em cada turno.

Esses sinais não precisam decidir sozinhos: eles decidem qual caminho o estouro toma. Cliente com perfil de crescimento legítimo que bate o teto merece continuar atendido com custo degradado enquanto o time comercial é acionado no mesmo dia. Cliente com perfil de laço automatizado que bate o teto merece corte imediato do fluxo caro, porque cada minuto adicional é prejuízo puro sem contrapartida. A pior configuração possível é a que trata os dois igual, e ela é a configuração padrão de quem só implementou um número máximo.

05

O que acontece quando o teto é atingido no meio de uma conversa

Devolver um erro de cota excedida no meio de um atendimento é a resposta tecnicamente correta e comercialmente péssima, porque quem paga o preço é o consumidor final do seu cliente, que não tem relação nenhuma com o contrato estourado. A conversa em andamento merece tratamento diferente da conversa nova: cortar a que já começou desperdiça todo o custo já gasto nela e ainda entrega uma experiência quebrada, enquanto recusar a abertura de uma nova é contido e explicável. O corte também deve ser gradual em vez de binário, e a degradação pode acontecer bem antes do teto.

Consumo do orcamento no ciclo
0%                    70%        85%        100%      110%
|----------------------|----------|----------|---------|
   normal              aviso      degradado   fila     corte

  normal      modelo padrao, contexto completo, todas as ferramentas
  aviso       identico ao normal, mais alerta ao admin do cliente
  degradado   modelo barato, contexto curto, cache semantico agressivo
  fila        conversa nova espera ou vai direto para o humano
  corte       so conversa em andamento termina, nova e recusada

Regra de transicao
  desce de nivel  na hora, assim que o consumo cruza o limiar
  sobe de nivel   so no proximo ciclo ou apos aumento explicito de teto
  conversa ativa  mantem o nivel em que comecou ate o desfecho

A regra de manter a conversa no nível em que ela começou tem um motivo prático: alternar de modelo no meio de um atendimento produz uma quebra de tom e de qualidade que o cliente percebe como defeito, e o custo de terminar a conversa no modelo caro é pequeno e limitado comparado ao de reabrir o caso. Já a transição para baixo precisa ser imediata e sem histerese, porque aqui o recurso não se recupera: diferente de um sinal de carga, que oscila e volta ao normal sozinho, o consumo do orçamento é monotônico dentro do ciclo. Só sobe. Voltar ao nível anterior por conta própria significa gastar duas vezes o que já não cabia.

06

Avisar antes, não depois: o teto que só aparece no erro é um defeito de produto

A maior parte da insatisfação com limite de gasto não vem do limite em si, vem da surpresa. O cliente que recebe um aviso aos setenta por cento com projeção de quando vai estourar tem tempo de escolher entre otimizar o uso, subir de plano ou aceitar a degradação, e nenhuma dessas três conversas é ruim. O cliente que descobre o teto pelo erro em produção passa a tratar o seu produto como imprevisível, e essa percepção não se conserta com um crédito. O aviso precisa de três coisas para ser útil: chegar ao administrador do contrato e não ao usuário final, trazer projeção em vez de apenas o percentual atual e apontar o que está consumindo.

// spend-forecast.js
// Projecao simples de estouro por ritmo recente e escolha do nivel.
// Ritmo dos ultimos dias vence a media do ciclo: quem dobrou o uso
// ontem estoura muito antes do que a media do mes sugere.

export const projectOverrun = ({ spentCents, limitCents, dailySpendCents, daysLeftInCycle }) => {
  const recent = dailySpendCents.slice(-7);
  if (!recent.length) return { willOverrun: false, reason: 'sem historico suficiente' };

  // Media ponderada: o dia mais recente pesa mais que o de uma semana atras.
  const weights = recent.map((_, i) => i + 1);
  const weightSum = weights.reduce((a, b) => a + b, 0);
  const rate = recent.reduce((acc, value, i) => acc + value * weights[i], 0) / weightSum;

  const remaining = limitCents - spentCents;
  if (rate <= 0) return { willOverrun: false, reason: 'sem consumo recente' };

  const daysToLimit = remaining / rate;
  const projectedCents = Math.round(spentCents + rate * daysLeftInCycle);

  return {
    willOverrun: daysToLimit < daysLeftInCycle,
    daysToLimit: Math.max(0, Math.floor(daysToLimit)),
    projectedCents,
    projectedPercent: Math.round((projectedCents / limitCents) * 100),
    dailyRateCents: Math.round(rate),
  };
};

const LEVELS = [
  { name: 'corte', minPercent: 110 },
  { name: 'fila', minPercent: 100 },
  { name: 'degradado', minPercent: 85 },
  { name: 'aviso', minPercent: 70 },
  { name: 'normal', minPercent: 0 },
];

// O nivel efetivo e o pior entre o consumo real e a projecao: quem esta
// em 40% mas estoura em tres dias ja precisa ser avisado hoje, nao no
// dia em que cruzar 70%.
export const resolveLevel = ({ spentCents, limitCents, forecast }) => {
  const percent = (spentCents / limitCents) * 100;
  const byActual = LEVELS.find((level) => percent >= level.minPercent);

  if (forecast?.willOverrun && byActual.name === 'normal') {
    return { level: 'aviso', percent: Math.round(percent), trigger: 'projecao' };
  }
  return { level: byActual.name, percent: Math.round(percent), trigger: 'consumo' };
};

O detalhe que faz esse aviso funcionar é a projeção mandar no nível junto com o consumo. Um cliente em quarenta por cento do orçamento no dia cinco do ciclo parece confortável olhando só o percentual, e se ele triplicou o ritmo nos últimos dois dias vai estourar antes do dia dez. Avisar aos setenta por cento nesse caso é avisar tarde demais para qualquer reação. Por isso a média dos últimos dias pesa mais que a média do ciclo inteiro: o que importa não é quanto ele gastou, é a velocidade com que está gastando agora.

07

Testar o limite antes que ele seja testado pela fatura

Controle de gasto é código que só executa nas bordas, e por isso é onde os bugs sobrevivem por mais tempo: se nenhum cliente chegou perto do teto neste mês, todo o caminho de degradação e corte ficou sem executar. Quando finalmente executa, executa na pior hora possível, sem ninguém tendo visto aquele fluxo funcionar. A boa notícia é que esse é um dos poucos mecanismos de resiliência que dá para exercitar sem nenhum risco, porque o gatilho é um número em um contador e não um provedor real caindo.

  1. Teste de concorrência na reserva: dispare cem reservas paralelas contra um orçamento que só cabe dez e verifique que exatamente dez foram concedidas, com o resto recusado. É aqui que a implementação sem operação atômica falha, e ela passa em qualquer teste sequencial.
  2. Teste de expiração de reserva: force o acerto de uma reserva já expirada e confirme que o custo real entrou no consumido mesmo assim, sem duplicar o débito e sem sumir com ele.
  3. Teste de cancelamento: aborte a chamada ao provedor depois da reserva e antes da resposta, e confirme que o saldo voltou integralmente em vez de ficar preso até o TTL.
  4. Teste de transição de nível: injete o consumido em cada faixa e verifique que o modelo escolhido, o tamanho de contexto e a decisão sobre conversa nova mudam conforme o esperado, incluindo a regra de que a conversa em andamento mantém o nível de origem.
  5. Teste de projeção: alimente uma série de gasto diário em rampa e confirme que o aviso dispara pela projeção antes do percentual bater no limiar.
  6. Ensaio no ambiente real: escolha um cliente interno, baixe o teto dele para um valor que ele vai cruzar no mesmo dia e acompanhe a experiência ponta a ponta, incluindo o aviso que chega e o que o usuário final vê quando o nível desce.

O último item é o que mais rende, porque ele testa a parte que nenhum teste automatizado alcança: se o aviso chega a alguém que pode agir, se a mensagem exibida ao usuário final faz sentido para quem não conhece o contrato e se a degradação é perceptível a ponto de gerar reclamação. Já vi um sistema em que toda a mecânica funcionava perfeitamente e o alerta ia para um endereço de e-mail que ninguém lia havia um ano. O contador estava certo, o corte foi no momento certo e o cliente foi surpreendido do mesmo jeito.

FAQ

Perguntas frequentes

Rate limit por requisição não resolve o problema de gasto?

Não, porque ele mede a variável errada. Rate limit conta eventos em uma janela curta e trata todas as requisições como equivalentes, enquanto o custo real varia com facilidade em uma ou duas ordens de grandeza entre uma pergunta curta respondida por um modelo pequeno e uma conversa longa que arrasta trechos de RAG e chama ferramentas. Para aproximar dinheiro por contagem você precisaria calibrar o teto pela requisição mais cara possível, o que estrangula quem faz uso barato, ou pela média, o que deixa passar exatamente o padrão que gera prejuízo. Além disso, as janelas são incompatíveis: rate limit se renova sozinho a cada minuto e o orçamento se acumula ao longo do ciclo de faturamento sem voltar. Os dois controles são complementares e devem coexistir, o rate limit protegendo a capacidade compartilhada e o teto de gasto protegendo a margem do contrato, cada um com o próprio contador e a própria reação ao estouro.

Como impedir que requisições concorrentes furem o teto?

Reservando o custo estimado antes da chamada, de forma atômica, e acertando pelo custo real depois. Se você debita apenas quando a resposta chega, todas as requisições paralelas leem o mesmo saldo ainda intacto e se autorizam juntas, e o estouro só aparece quando elas retornam. A reserva funciona como a pré-autorização de um cartão: você estima conservadoramente usando os tokens de entrada, que conhece exatamente, mais o teto de max tokens para a saída, reserva esse valor em uma operação que lê e escreve em um passo só, e depois substitui a reserva pelo custo real devolvendo a diferença. Dois cuidados definem se isso funciona em produção: o TTL da reserva precisa ser maior que o timeout máximo da chamada, senão a reserva expira durante o streaming, e o acerto de uma reserva já expirada precisa ainda assim somar o custo real ao consumido, porque o dinheiro foi gasto de verdade e ignorá-lo faz o gasto sumir justamente nas chamadas mais longas e caras.

Como saber se um pico de uso é abuso ou o cliente crescendo?

Olhando a forma do consumo e não o volume, porque as duas curvas sobem igual. Quatro sinais baratos separam bem os casos: diversidade de conteúdo, já que abuso automatizado repete o mesmo prompt e produz taxa alta de acerto em cache semântico com gasto subindo; distribuição no tempo, porque uso humano tem vale noturno e queda no fim de semana enquanto script tem consumo plano nas vinte e quatro horas; razão entre conversas e usuários distintos, que cresce junto na adoção real e descola no laço automatizado; e desfecho das conversas, já que adoção produz resolução enquanto laço produz conversa abandonada no meio. Esses sinais não precisam decidir o bloqueio sozinhos, eles escolhem qual caminho o estouro toma: perfil de crescimento legítimo continua atendido em modo degradado com o time comercial acionado no mesmo dia, e perfil de laço tem o fluxo caro cortado na hora, porque ali cada minuto extra é prejuízo sem contrapartida.

O teto que ninguém vê chegar é o que gera cancelamento

Limite de gasto é um controle de margem, não de capacidade, e por isso pede contador em dinheiro com reserva antes da chamada, acerto pelo custo real depois e uma decisão explícita sobre quanto estouro vale a pena trocar por latência. O que separa a implementação que protege o negócio da que perde cliente não é o número escolhido: é ter níveis de degradação antes do corte, distinguir abuso de crescimento pela forma do consumo, manter a conversa em andamento no nível em que começou e avisar por projeção enquanto ainda dá tempo de reagir. Posso implementar esse controle de gasto por cliente no seu sistema com LLM, da reserva atômica no caminho quente ao alerta com projeção que chega a quem decide, para que a fatura pare de ser descoberta no fim do mês.