← Voltar para o Blog

Fila e limite de taxa: quando a API de IA te barra

O erro 429 aparece no pior momento, e a reação instintiva — tentar de novo imediatamente — piora tudo. O desenho que absorve pico sem derrubar a experiência.

Equipe EasyOps Cloud · · 7 min de leitura

429 Too Many Requests. A campanha funcionou, o volume subiu, e a API de IA começou a recusar. A reação instintiva é tentar de novo na hora — o que, com todos os clientes fazendo o mesmo, transforma um limite atingido em uma parede.

O desenho que absorve isso não é complicado, e a maior parte dele vale independentemente de IA. O que muda são as unidades: aqui o limite não é só de requisições, é de tokens.

Os limites que existem

Fornecedores de API costumam impor pelo menos dois:

  • Requisições por minuto. O mais óbvio.
  • Tokens por minuto, somando entrada e saída. É o que mais surpreende, porque uma única chamada com contexto grande pode consumir uma fatia relevante da cota.

Isso tem uma consequência que muda o desenho: reduzir o tamanho do prompt aumenta a sua capacidade tanto quanto reduzir o número de chamadas. Um contexto enxuto não economiza apenas dinheiro — economiza cota.

As respostas costumam trazer os cabeçalhos com o estado atual:

resp.headers.get("x-ratelimit-remaining-requests")
resp.headers.get("x-ratelimit-remaining-tokens")
resp.headers.get("x-ratelimit-reset-tokens")
resp.headers.get("retry-after")

Ler esses valores e agir antes de bater no limite é bem melhor que reagir ao erro. Quando o restante cair abaixo de um percentual, vale desacelerar proativamente.

Retry com espera crescente

Tentar de novo imediatamente é o pior comportamento possível. A espera precisa crescer, e precisa ter variação aleatória:

import random, time

def chamar_com_retry(fn, tentativas=5):
    for i in range(tentativas):
        try:
            return fn()
        except RateLimitError as e:
            if i == tentativas - 1:
                raise
            espera = getattr(e, "retry_after", None)
            if espera is None:
                espera = min(2 ** i, 32) + random.uniform(0, 1)
            time.sleep(espera)
        except (Timeout, ServerError):
            if i == tentativas - 1:
                raise
            time.sleep(min(2 ** i, 32) + random.uniform(0, 1))

Três detalhes importam. O retry-after do fornecedor, quando existe, é melhor que qualquer estimativa sua. A variação aleatória evita que todos os clientes voltem no mesmo instante — sem ela, você cria ondas sincronizadas que batem no limite juntas. E o teto na espera impede que a quinta tentativa demore minutos.

Repare também no que não deve ter retry: erro de validação, autenticação inválida, e requisição malformada. Tentar de novo o que vai falhar de novo consome cota e atrasa o que poderia funcionar.

Fila, que é a resposta estrutural

Retry resolve o pico curto. Volume sustentado acima da cota exige fila.

O desenho separa recebimento de processamento: a aplicação aceita o pedido, devolve um identificador, e um número controlado de workers consome no ritmo que a cota permite.

# Produtor: aceita e enfileira
def solicitar(pergunta, usuario):
    tarefa_id = uuid4().hex
    fila.push({"id": tarefa_id, "pergunta": pergunta,
               "usuario": usuario, "prioridade": prioridade_de(usuario)})
    return tarefa_id

# Consumidor: respeita a cota
LIMITE = Semaphore(8)          # concorrência máxima

def worker():
    while True:
        t = fila.pop()
        with LIMITE:
            resultado = chamar_com_retry(lambda: modelo.gerar(t["pergunta"]))
        guardar(t["id"], resultado)
        notificar(t["usuario"], t["id"])

A concorrência máxima é a variável de controle. Ela deve ser calibrada pela cota, e não pela capacidade da sua máquina — de nada adianta trinta workers se a API aceita oito chamadas simultâneas.

Prioridade evita o pior cenário

Fila única tem um problema: um lote grande enfileirado às 9h faz o usuário que perguntou às 9h05 esperar atrás de tudo.

A separação por prioridade resolve:

FilaO que entraComportamento
InterativaPessoa esperando respostaSempre primeiro
PadrãoProcessamento assíncronoRitmo normal
LoteReprocessamento, indexaçãoSó com folga

A regra é simples: nunca deixar trabalho de lote bloquear interação. Reprocessar cinquenta mil documentos é legítimo e deve acontecer no espaço que sobra — e deve ser pausado automaticamente quando a fila interativa crescer.

Um cliente não pode consumir tudo

Sem limite por cliente, um único usuário — ou uma integração com laço — consome a cota inteira e derruba o serviço para todos.

def permitir(usuario):
    chave = f"ia:cota:{usuario}:{int(time.time() // 60)}"
    n = redis.incr(chave)
    if n == 1:
        redis.expire(chave, 120)
    return n <= LIMITE_POR_MINUTO[plano_de(usuario)]

Esse contador por janela de minuto é simples e suficiente. Quando o limite for atingido, a resposta certa não é erro genérico — é informar quando será possível tentar de novo, no mesmo espírito do artigo sobre rate limiting no nginx.

Vale contar tokens, e não só requisições, para os clientes de maior volume. Uma requisição com contexto enorme consome muito mais que dez pequenas.

Degradar em vez de falhar

Quando a capacidade acaba, há alternativas melhores que devolver erro:

  • Servir do cache, mesmo com resposta um pouco antiga. O artigo sobre cache semântico trata do mecanismo.
  • Cair para um modelo menor, mais barato e com cota separada. Resposta um pouco pior é melhor que nenhuma.
  • Assumir o modo assíncrono explicitamente: "estamos com volume alto, sua resposta chega em alguns minutos" com notificação depois.
  • Transferir para humano, em atendimento.

A pior opção é a mensagem genérica de erro, que não informa nada e leva a pessoa a tentar de novo — aumentando a carga justamente quando ela é o problema.

O que acompanhar

Quatro indicadores dizem se o arranjo está saudável:

  • Tamanho da fila por prioridade. Crescimento sustentado significa capacidade insuficiente, não pico.
  • Tempo de espera até o processamento começar.
  • Taxa de 429 e quantas tentativas foram necessárias.
  • Utilização da cota, pelos cabeçalhos do fornecedor.

O primeiro é o mais informativo. Fila que sobe e desce ao longo do dia é normal e saudável — é ela absorvendo pico, que é a função. Fila que sobe e nunca esvazia significa que a demanda supera a capacidade, e aí o caminho é aumentar a cota, reduzir o consumo por chamada, ou processar menos.

Reduzir o consumo antes de pedir mais cota

Antes de solicitar aumento de limite, vale colher o que costuma estar sobrando:

  • Prompt enxuto. Contexto que não é usado é cota gasta. Revisar o que vai em cada chamada frequentemente corta um terço.
  • Menos trechos recuperados. Cinco bem escolhidos costumam superar vinte mediocres, e consomem um quarto.
  • Cache, que elimina a chamada inteira.
  • Modelo menor para tarefa simples. Classificar e extrair não precisa do maior modelo, e o roteamento por tarefa é a alavanca mais eficaz — assunto do artigo sobre API ou modelo próprio.
  • Truncar resultado de ferramenta, que entra no contexto e conta na cota.

Essas cinco medidas costumam liberar mais capacidade que um aumento de cota, e não dependem de negociação com ninguém. Vale esgotá-las antes de tratar o limite como restrição externa — na maioria das vezes, o gargalo é consumo desnecessário, não teto do fornecedor.

A comunicação com quem está esperando

Um aspecto de produto que decide a percepção de tudo isso. Fila bem construída com comunicação ruim é percebida como sistema lento; fila igual com comunicação clara é percebida como sistema organizado.

Três informações mudam a experiência:

Confirmação imediata. O pedido foi recebido, tem identificador, não se perdeu. É a diferença entre esperar e não saber se algo aconteceu.

Posição ou estimativa. "Sua solicitação é a terceira da fila" ou "resposta em aproximadamente dois minutos". Estimativa aproximada e honesta funciona muito melhor que nenhuma — e melhor ainda que uma otimista que não se cumpre.

Notificação quando terminar, para que a pessoa não precise ficar olhando.

Vale distinguir claramente as duas situações no texto: "estamos com volume alto" é diferente de "houve um erro". A primeira é uma espera com fim previsto; a segunda pede outra ação. Confundi-las leva quem esperaria tranquilamente a tentar de novo — e cada nova tentativa aumenta exatamente a fila que causou a espera.

O caso do modelo próprio

Vale notar que a fila continua necessária mesmo sem API externa. Com modelo local, o limite deixa de ser uma cota contratual e passa a ser físico: quantas inferências simultâneas a máquina sustenta.

A diferença é que ninguém devolve 429 — a máquina simplesmente fica lenta para todos, ou estoura a memória e derruba o serviço. É um modo de falha pior, porque degrada em vez de recusar.

Por isso o controle de concorrência é ainda mais importante nesse cenário. Um semáforo limitando as inferências simultâneas ao que a máquina comporta transforma sobrecarga em espera ordenada, que é exatamente o comportamento desejado.

O número certo vem de medição, não de estimativa: aumente a concorrência até a latência por requisição começar a subir de forma desproporcional, e fique um degrau abaixo. É o mesmo raciocínio de dimensionamento do artigo sobre rodar LLM na sua VPS.

Suba um servidor em minutos

Preço em real, suporte em português e dados no Brasil.

Criar conta