Tratamento de 429: fila, backoff e estados claros para o usuário
Tratamento de erro 429 para apps LLM e APIs: enfileire requisições, faça backoff com segurança e mostre estados claros ao usuário para manter o produto previsível.

O que um 429 realmente significa (e por que os usuários notam)
Um 429 é o provedor dizendo: desacelere. Você está enviando muitas requisições muito rápido. Normalmente não significa que seu código está “errado.” Significa que você atingiu um limite definido para proteger os sistemas deles (e às vezes seu próprio orçamento).
Os usuários percebem mal o tratamento de 429 mesmo quando o app funciona na maior parte do tempo. A experiência vira confusa: um spinner que nunca termina, um erro após longa espera, ou respostas que às vezes aparecem e às vezes falham. A pior parte é a inconsistência. As pessoas não conseguem prever o que vai acontecer em seguida.
Retries podem ajudar, mas retries descuidados muitas vezes pioram a situação. Se cada requisição falhada tenta novamente imediatamente, você cria um pico de tráfego exatamente quando o provedor está pedindo para reduzir a carga. Isso pode transformar um pequeno problema em uma interrupção maior e queimar cotas rapidamente.
O objetivo não é velocidade perfeita. É comportamento previsível sob carga. O app deve se comportar do mesmo jeito toda vez, mesmo que a resposta chegue mais tarde.
Um modelo mental simples são três peças trabalhando juntas:
- Uma fila para suavizar surtos em vez de disparar tudo ao mesmo tempo
- Backoff (geralmente backoff exponencial com jitter) para espalhar retries
- Estados claros para o usuário, para que as pessoas saibam se a requisição está aguardando, tentando novamente ou precisa de ação
Exemplo: um app de chat tem um pico pela manhã. Sem controle, metade das mensagens falham aleatoriamente. Com fila e backoff, as mensagens podem demorar mais, mas os usuários veem “Em fila” e depois “Tentando novamente em 4s.” O app parece calmo e confiável em vez de quebrado.
De onde vêm os limites em apps reais
A maioria dos 429s não são quedas aleatórias. São um sinal de que muitas requisições atingiram um limite ao mesmo tempo e seu app não espalhou a carga.
O gatilho mais comum é um burst: um pico de tráfego após um lançamento, uma postagem social ou um cliente grande convidando um time. Jobs em lote também fazem isso, como reindexar conteúdo, importar CSVs ou recalcular embeddings à noite.
Também importa de quem é o limite. Provedores costumam impor limites por chave de API, por conta ou por região. Seu próprio app pode criar limites também, intencionalmente ou por acidente, como por usuário, por workspace ou por IP (especialmente se muitos usuários estão atrás de um NAT corporativo).
Apps LLM têm alguns padrões que atingem limites mais rápido do que você imagina:
- Respostas em streaming mantêm conexões abertas por mais tempo, então a concorrência sobe mesmo quando o QPS parece normal.
- Chamadas a ferramentas multiplicam trabalho, porque uma “resposta” pode disparar chamadas extras para buscar dados, executar funções e depois consultar o modelo novamente.
- Embeddings em massa são clássicos: um único botão “importar documentos” pode disparar centenas de requisições em um loop apertado.
Multiplicadores ocultos são o culpado mais comum. Um clique pode gerar muitas chamadas a jusante:
- Enviar um chat dispara moderação, a completion principal e uma chamada de logging
- Uma única mensagem do usuário pode acionar 3–10 chamadas de ferramenta antes da resposta final
- Um botão “retry” dispara imediatamente e adiciona pressão
- O carregamento de uma página dispara várias requisições paralelas em abas
- Um worker em background repete o mesmo job falho em muitas contas
O staging costuma parecer bem porque tem um usuário, dados limpos e nenhum cron job. Produção tem concorrência, picos reais, sessões de streaming de longa duração e tempestades acidentais como vários workers iniciando o mesmo backfill.
Escolha uma política clara: retry, aguardar ou parar
Quando você recebe um 429, a pior atitude é “tentar de novo imediatamente” sem regras. Isso faz o provedor empurrar de volta com mais força e seu app fica aleatório. Um bom tratamento de 429 começa com uma política simples que toda a equipe pode seguir.
Separe requisições em dois buckets:
- Ações interativas (o usuário clicou um botão)
- Trabalho em background (syncs, webhooks, jobs em lote)
Ações interativas precisam de uma janela curta de paciência e feedback claro. Trabalho em background pode esperar mais, desde que não fique acumulando para sempre.
Uma política prática que você pode adotar e ajustar depois:
- Repetir apenas quando a requisição for segura para repetir (chamadas somente leitura, ou writes com idempotência).
- Definir uma janela máxima de retry (por exemplo, 10–20 segundos para interativo, 5–30 minutos para background).
- Parar e falhar rápido quando o usuário pode corrigir mudando o comportamento (como ficar apertando “Regenerate”) ou quando falta input obrigatório.
- Usar prioridades separadas: requisições interativas vão na frente, trabalho em background cede quando os limites apertam.
- Logar contexto em todo 429 para que você possa depurar padrões, não chutar no escuro.
Idempotência é o que torna retries seguros. Se uma operação cria algo (cobrar um cartão, criar um registro, enviar um email), anexe uma chave de idempotência para que um retry não duplique o efeito colateral. Se não conseguir tornar idempotente, prefira “parar e perguntar” em vez de retries cegos.
Defina “feito” para cada requisição. Uma requisição deve ou ter sucesso, ou retornar um erro claro, ou expirar depois da sua janela máxima de retries. Ficar pendurado para sempre é o resultado mais frustrante.
Logue o suficiente para responder o básico: qual endpoint foi chamado, qual usuário disparou, que tipo de requisição era (interativa vs background), quando começou e quanto esperou.
Passo a passo: adicione uma fila de requisições que suavize picos
A maneira mais rápida de melhorar o tratamento de 429 é parar de disparar requisições no momento em que a UI as aciona. Em vez disso, coloque cada chamada de API numa fila. Isso transforma um surto de 50 cliques em um fluxo controlado que o provedor pode realmente aceitar.
1) Comece com uma fila pequena entre sua UI e o provedor
Mantenha a primeira versão sem frescura. Quando o app quer chamar o LLM ou uma API, ele cria um job com o payload e algumas etiquetas (quem pediu, para qual tela, quando foi criado). A UI envia jobs para a fila, não diretamente para o provedor.
Um formato prático de job inclui: endpoint/modelo, prompt ou hash de parâmetros, ID do usuário, prioridade e um contador de retries.
2) Adicione um worker com limite de concorrência
Um worker puxa jobs da fila e os executa. A configuração chave é concorrência: quantos jobs podem rodar ao mesmo tempo. Comece baixo (como 1 a 3) e aumente só se vir resultados estáveis.
Uma abordagem simples que funciona para a maioria dos apps:
- Procese apenas N jobs em paralelo (seu limite de concorrência)
- Quando um job termina, inicie imediatamente o próximo
- Se um job receber 429, coloque-o de volta na fila com um atraso (o backoff vem depois)
- Acompanhe jobs ativos para nunca exceder N
3) Prioridades: mantenha o app responsivo
Nem todas as requisições são iguais. Um botão que o usuário acabou de clicar deve pular na frente de trabalho em background como “resumir tudo” ou “gerar relatório semanal.” Use duas prioridades (alta e baixa) no começo. Se sua fila suportar, trate prioridade alta como uma via separada.
4) Deduplique requisições repetidas
Usuários dão double-click. Frontends re-renderizam. Se você enviar prompts idênticos dentro de uma janela curta (digamos 2 a 10 segundos), una-os. Retorne o mesmo resultado em andamento para todos os chamadores. Isso sozinho pode reduzir o volume de requisições drasticamente.
5) Persista o estado da fila para que reinícios não percam trabalho
Se o servidor reiniciar, você não quer que jobs pendentes desapareçam ou se repitam de forma imprevisível. Armazene jobs enfileirados e seu status em armazenamento durável (banco de dados ou um sistema de filas adequado). Muitos protótipos falham aqui: requisições são fire-and-forget, e usuários veem respostas faltando, duplicatas ou spinners infinitos quando os limites aparecem.
Passo a passo: backoff que reduz pressão em vez de aumentá-la
Um bom tratamento de 429 é, na maior parte, sobre fazer menos, não tentar mais. Se você retryar rápido demais, transforma uma pequena lentidão em um pico de tráfego e mantém usuários presos em loop.
Comece com backoff exponencial: após o primeiro 429, espere um pouco, depois espere mais a cada 429 seguinte. Uma regra simples é dobrar o atraso a cada vez. Isso dá tempo para o provedor se recuperar e evita que seu app continue martelando a API.
Adicione jitter (um pequeno offset aleatório) em cada espera. Sem jitter, milhares de clientes que atingiram o limite ao mesmo tempo vão retryar ao mesmo tempo e bater no limite de novo. Jitter espalha os retries para que seu app tenha chance de sucesso.
Se o provedor der uma dica como um valor Retry-After, trate-o como a verdade básica. Use-o como atraso base e então aplique um pequeno jitter ao redor dele.
Defina tetos claros para que retries não durem para sempre:
- Tentativas máximas (por exemplo 3–6 tries)
- Atraso máximo (por exemplo limite de 30–60 segundos)
- Orçamento total de tempo (pare após, por exemplo, 2 minutos)
- Um caminho de fallback (salve a tarefa, peça ao usuário para tentar mais tarde)
Pare de tentar em erros que não vão se resolver sozinhos. Um 400 vai falhar sempre. Um 401/403 significa que a autenticação está quebrada. Retry apenas quando for provável que funcione (429, muitos 5xx e alguns timeouts de rede).
Exemplo: um recurso de chat recebe 429 durante um lançamento. Tentativa 1 espera 1–2s, tentativa 2 espera 2–4s, tentativa 3 espera 4–8s, então você para e mostra uma mensagem clara de que o sistema está ocupado e a mensagem será enviada quando estiver disponível.
Estados visíveis ao usuário que mantêm o app previsível
Quando você chega num limite, o pior não é o atraso. É a confusão. Se a UI parecer travada ou ficar alternando entre erros e spinners, os usuários vão clicar de novo, atualizar a página ou abrir outra aba. Isso cria mais requisições e prolonga o problema.
Um bom tratamento de 429 começa por nomear o que está acontecendo em linguagem simples e dar um único próximo passo claro.
Use um pequeno conjunto de estados claros
Mantenha os estados consistentes pelo app. A maioria das equipes precisa de apenas três:
- Aguardando na fila: “Estamos na fila para enviar sua requisição.”
- Tentando novamente: “O provedor está ocupado. Tentando novamente em 10–20 segundos.”
- Tentar novamente: “Não conseguimos enviar ainda. Por favor, tente novamente agora.”
Adicione uma expectativa de tempo sempre que puder, mesmo que seja apenas uma faixa. Você pode basear isso na posição na fila (por exemplo, 3 requisições na frente) ou no seu timer de backoff. Uma pequena contagem regressiva ajuda as pessoas a esperar sem clicar.
Ofereça um botão Cancelar seguro para esperas longas. Explique o que cancelar significa em uma frase: “Cancelar interrompe os retries. Seu rascunho fica aqui e nada é enviado.” Se cancelar faria perder trabalho, diga isso claramente e ofereça “Salvar rascunho” em vez disso.
Evite erros brutos como “429” ou “Too Many Requests.” Traduza para o que o usuário se importa: “O provedor de IA está pedindo para reduzirmos o ritmo.” Depois ofereça uma ação: continuar esperando (padrão) ou tentar novamente.
Para esperas maiores que alguns segundos, mostre uma notificação leve quando estiver pronto, como um banner in-app que diga “Sua resposta está pronta” ou uma mudança de status na mensagem. Isso é especialmente importante em chat.
Proteja o resto do seu app enquanto o provedor te limita
Quando um provedor começa a retornar 429s, o maior risco não é a chamada falhada. É o efeito cascata: threads ficam presos, filas crescem sem limite e partes não relacionadas do app ficam lentas. Um bom tratamento de 429 é principalmente sobre contenção.
Comece com timeouts em todo lugar onde uma chamada a provedor pode travar. Um retry que espera para sempre não é paciente, é um bloqueador. Tenha um tempo máximo claro por tentativa e um tempo máximo total entre retries, para que uma requisição não possa segurar o resto do sistema refém.
Um circuit breaker ajuda quando 429s disparam. Em vez de deixar todas as requisições baterem no provedor, pause chamadas por uma janela curta e então teste com uma requisição pequena. Isso torna a desaceleração previsível e impede que você adicione mais pressão.
Separe a responsividade da UI da velocidade do provedor. A UI deve reagir instantaneamente: aceitar a ação do usuário, mostrar um estado claro e processar o trabalho em background. Se você ligar cliques de botão ao tempo de resposta do provedor, o app inteiro parece quebrado mesmo quando só uma dependência está lenta.
Orçamentos por usuário evitam que um usuário pesado (ou um bug) consuma os outros. Mantenha simples: quantos jobs enfileirados e quanto tempo de retry um usuário pode consumir antes de você começar a atrasar ou rejeitar.
Sinais úteis para monitorar:
- Taxa de 429 ao longo do tempo (picos vs contínua)
- Profundidade da fila (quantos jobs aguardando)
- Tempo médio de espera antes de um job começar
- Taxa de timeouts (muito estrito vs muito folgado)
- Tempo com circuit breaker aberto (com que frequência você pausa chamadas)
Erros comuns que pioram problemas de 429
A maior parte da dor com 429 vem de alguns erros previsíveis. Evite-os e o tratamento de 429 vira algo chato (no bom sentido).
Retries que criam uma tempestade de retries
Retries instantâneos, ou com o mesmo atraso fixo toda vez, podem transformar uma pequena lentidão em uma queda. Se 200 usuários clicarem “Tentar novamente” e cada cliente retryar no mesmo segundo, você tem uma onda sincronizada de tráfego que continua gerando 429s.
Use um atraso que cresça com o tempo, mais jitter, para espalhar os retries.
Retries sem fim e custos ocultos
Sem um limite de tentativas e um tempo máximo total, uma única ação do usuário pode disparar um loop sem limite. Isso pode queimar tokens ou créditos de API, manter workers ocupados fazendo nada útil e criar telas de “carregando para sempre”.
Limite os retries e pare com uma mensagem clara quando atingir o limite.
Um limite global sem prioridades
Se você tratar todas as chamadas igual, trabalho em background (como embeddings) pode sufocar fluxos críticos como login, checkout ou “Enviar mensagem.” Use filas ou prioridades separadas para que ações essenciais sempre ganhem.
Ignorar sucesso parcial
Um caso comum: uma requisição succeed (você criou uma mensagem), mas a próxima recebe 429 (falha ao buscar o histórico atualizado). Se tratar a operação inteira como falha, você corre o risco de duplicatas, updates de UI faltando ou estado quebrado.
Trate cada chamada como um passo separado. Salve o que deu certo, retry apenas o que falhou.
Erros genéricos que ensinam usuários a clicar mais
“Algo deu errado” faz as pessoas apertarem o botão de novo, o que cria mais carga. Mostre um estado específico como: “Estamos aguardando o provedor. Tentando novamente em 8 segundos.”
Checklist rápido antes de lançar
Antes de release, trate o manejo de 429 como um recurso de produto, não só um loop de retries. Uma boa configuração controla custos, evita surpresas e faz o app parecer calmo mesmo quando o provedor diz “desacelere.”
Um checklist prático antes do lançamento:
- Concorrência está limitada em dois lugares: por feature (por exemplo, chat) e por usuário (para que uma pessoa ocupada não sobreponha todo mundo).
- Retries têm limites claros: contagem máxima de tentativas e tempo máximo total gasto em retries (para que requisições não pendurem para sempre).
- Backoff reduz pressão: você adiciona jitter, respeita dicas do servidor como
Retry-Aftere evita retries sincronizados entre muitos usuários. - A UI permanece previsível: usuários veem o que está acontecendo (“Aguardando para tentar novamente”), quanto tempo pode levar e uma ação segura (Cancelar, Tentar novamente ou Continuar sem esse resultado).
- Logs são suficientes para depurar rápido: inclua nome do provedor, endpoint/modelo, request ID, user ID (ou token anônimo), número da tentativa, tempo de espera escolhido e o payload de erro exato.
Faça um teste rápido de stress antes de lançar. Abra o app em dois navegadores, dispare 10–20 ações rapidamente e confirme que você vê enfileiramento ordenado, atrasos crescendo e uma parada limpa quando os limites são atingidos.
Exemplo: um chat que é atingido por um pico repentino
Imagine uma demo de time: alguém compartilha um link e, em um minuto, 50 pessoas abrem o chat e disparam o mesmo prompt. Seu app envia 50 chamadas quase idênticas para um provedor de LLM. Alguns segundos depois, você começa a receber 429s.
Sem controles, tudo parece aleatório. Algumas requisições expiram, outras retryam imediatamente e são bloqueadas de novo, e os usuários começam a clicar em Enviar repetidamente. Agora você tem requisições duplicadas, custo maior e saídas desencontradas (uma pessoa vê resposta, outra vê erro, outra vê duas respostas).
Uma fila de requisições muda o formato do tráfego. Em vez de espremer o provedor, você alinha requisições e as libera num ritmo constante. Adicione prioridade para manter o app responsivo: ações críticas da UI como cancelar uma mensagem, carregar histórico ou buscar perfil do usuário vão na frente, enquanto gerações aguardam sua vez.
O backoff impede que você piore a situação quando o provedor já está empurrando de volta. Ao receber 429, você retrya após um atraso que cresce (backoff exponencial) mais um pouco de aleatoriedade (jitter). Isso evita que os 50 clientes retryem exatamente ao mesmo tempo e batam de novo.
O que os usuários veem importa tanto quanto o código. Durante o pico, uma UI previsível pode mostrar:
- “Em fila” com espera estimada como 20–40 segundos
- Um botão Cancelar claro que realmente interrompe o job enfileirado
- Uma opção Retry somente após uma falha, não durante a espera normal
- Uma única mensagem por prompt (sem duplicatas), atualizando de Em fila → Enviando → Concluído
Próximos passos: implemente o básico e depois torne confiável
Comece com uma página que descreva suas regras: quando você retrya, quanto tempo espera, quando para e o que o usuário vê em cada etapa. Isso evita o caos comum onde o backend retrya para sempre enquanto a UI parece travada.
Se você só puder fazer duas tarefas de engenharia esta semana, faça-as no menor ponto que pare a dor primeiro: o cliente compartilhado que fala com seu provedor. Centralizar o tratamento de 429 faz com que toda feature se beneficie, e você não termina com cinco comportamentos de retry diferentes espalhados pelo app.
Uma ordem prática que geralmente funciona:
- Defina uma política de retry (tentativas máximas, tempo máximo de espera e quais erros são retryáveis)
- Adicione uma fila de requisições para suavizar picos vindos de cliques de usuário e jobs em background
- Adicione backoff exponencial com jitter para que retries reduzam pressão em vez de somar
- Adicione estados claros para o usuário: aguardando, tentando novamente e um fallback seguro quando você desiste
- Log cada 429 com o atraso escolhido para que você possa ver padrões depois
Depois faça um teste de carga simples. Simule 20–50 ações rápidas (enviar mensagem, regenerar, atualizar dados) e force alguns 429s. O objetivo não é velocidade perfeita. É que a UI continue compreensível: usuários devem ver que o app está esperando, não quebrado, e saber o que vem a seguir.
Se você está herdando um código gerado por IA, tenha cuidado para não empilhar patches. É assim que surgem tempestades de retries e requisições fantasmas que continuam rodando depois que o usuário navegou embora.
Se quiser uma segunda opinião, FixMyMess (fixmymess.ai) foca em diagnosticar e reparar apps gerados por IA que desmoronam sob tráfego real, incluindo retries descontrolados, caps ausentes e fan-out de requisições. Uma auditoria rápida pode ajudar você a chegar em comportamento previsível rápido.
Perguntas Frequentes
What does a 429 error actually mean?
Um 429 significa que o provedor está limitando sua taxa porque chegaram muitas requisições em pouco tempo. Seu código pode estar correto, mas o padrão de tráfego (picos, alta concorrência ou chamadas extras ocultas) está excedendo uma cota ou capacidade.
What should my app do when it gets a 429?
Por padrão, espere e tente novamente com um atraso — não retry imediato. Use uma janela curta de tempo para ações acionadas por usuário e uma janela maior para jobs em background. Pare de tentar quando atingir seu orçamento de tempo para que o app nunca fique girando indefinidamente.
Why are exponential backoff and jitter recommended?
Backoff exponencial faz com que cada retry espere mais tempo que o anterior, o que reduz a pressão quando o provedor já está sobrecarregado. Jitter adiciona uma pequena aleatoriedade para que muitos clientes não tentem novamente ao mesmo tempo e provoquem outra onda de 429s.
Should I follow the Retry-After header?
Trate Retry-After como a instrução principal de quanto tempo esperar. Se adicionar jitter, mantenha-o pequeno ao redor desse valor para respeitar o ritmo do provedor enquanto evita retries sincronizados.
Do I really need a request queue, or can I just retry?
Uma fila suaviza picos transformando “50 requisições agora” em um fluxo controlado que o provedor pode aceitar. Ela também oferece um lugar para prioridades, atrasos e persistência, evitando que requisições desapareçam ou sejam duplicadas em reinícios.
How do I pick a safe concurrency limit?
Comece com um limite de concorrência baixo (geralmente 1–3 por worker ou por feature) e aumente só depois de ver resultados estáveis em tráfego parecido com produção. O número certo é o maior valor que não dispara 429s frequentes durante picos normais.
When are retries dangerous, and how do I make them safe?
Retries são perigosos quando repetem operações que criam efeitos colaterais (cobranças, emails, registros). Faça somente retry de requisições seguras para repetir (leitura ou writes com chaves de idempotência). Se não for possível, prefira falhar rápido e pedir confirmação do usuário.
Should I dedupe identical prompts or API calls?
Sim. Se você detectar requests idênticos em janela curta, retorne o mesmo resultado em andamento para todos ao invés de iniciar chamadas novas. Isso reduz tráfego causado por double-clicks ou re-render do frontend.
What should users see while a request is waiting or retrying?
Use estados claros e consistentes como “Em fila”, “Tentando novamente em X segundos” e “Tente novamente” quando você desistir. O importante é que a UI reconheça a ação imediatamente e explique o que está acontecendo para evitar cliques repetidos que aumentam a carga.
What should I log and measure to fix 429s for good?
Registre contexto suficiente para ver padrões: qual endpoint/modelo, qual usuário ou workspace, interativo vs background, contagem de tentativas, atraso escolhido e tempo total gasto. Se você herdou um código gerado por IA com retries descontrolados, fan-out de chamadas ou limites ausentes, FixMyMess pode auditar e corrigir os modos de falha para deixar o comportamento previsível.