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:
| Fila | O que entra | Comportamento |
|---|---|---|
| Interativa | Pessoa esperando resposta | Sempre primeiro |
| Padrão | Processamento assíncrono | Ritmo normal |
| Lote | Reprocessamento, indexação | Só 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.