Voltar ao blog

Rate limit, timeout e retry: o que quebra quando seu app de IA ganha usuários

Erro 429, sobrecarga do provedor e função que estoura o tempo derrubam app de IA no primeiro pico de uso. Veja como implementar retry com backoff, fila e limite por usuário.

C
Caio Braga
26 de agosto de 2026 · 6 min de leitura
Sumário do artigo
Rate limit, timeout e retry: o que quebra quando seu app de IA ganha usuários

Seu app funcionou perfeitamente enquanto só você usava. No dia em que dez pessoas clicaram ao mesmo tempo, metade viu erro. O código não mudou. O que mudou foi a concorrência, e nenhum app gerado por IA nasce preparado para ela.

Três coisas quebram nesse momento: o limite de requisições do provedor de IA, o tempo máximo de execução da sua função e a ausência de qualquer fila entre o clique do usuário e a chamada cara.

Os erros que você vai ver

Status Nome Significado Tem retry?
400 Bad request Sua requisição está malformada Não
401 Unauthorized Chave errada ou ausente Não
403 Forbidden Sem permissão para o recurso Não
429 Rate limit Você passou do limite de requisições ou de tokens Sim, com espera
500 Server error Erro do lado do provedor Sim
529 Overloaded API sobrecarregada no momento Sim
Timeout Sem resposta a tempo Sua função morreu antes de a resposta chegar Depende

A regra é simples: erro na casa dos 400 é problema seu e retry só piora. Erro na casa dos 500 e o 429 são temporários e merecem nova tentativa.

Ler os headers antes de apanhar

A API da Anthropic devolve o estado do seu limite em headers de resposta. Ler esses valores evita descobrir o teto por tentativa e erro.

Header O que informa
anthropic-ratelimit-requests-remaining Requisições restantes na janela
anthropic-ratelimit-tokens-remaining Tokens restantes na janela
anthropic-ratelimit-requests-reset Quando a janela de requisições reinicia
retry-after Quantos segundos esperar antes de tentar de novo

Quando vier retry-after, respeite o número. Tentar antes disso conta como nova violação.

Retry com backoff exponencial

Retry sem espera crescente transforma um problema pequeno em tempestade: todo mundo tenta ao mesmo tempo, o provedor recusa, todo mundo tenta de novo no mesmo instante.

async function chamarComRetry(fn, tentativas = 4) {
  for (let i = 0; i < tentativas; i++) {
    try {
      return await fn();
    } catch (erro) {
      const status = erro.status;
      const temporario = status === 429 || status === 529 || status >= 500;

      // erro definitivo: falha na hora, sem insistir
      if (!temporario || i === tentativas - 1) throw erro;

      const retryAfter = Number(erro.headers?.['retry-after']);
      const espera = retryAfter
        ? retryAfter * 1000
        : Math.min(1000 * 2 ** i, 16000) + Math.random() * 500; // jitter

      await new Promise(r => setTimeout(r, espera));
    }
  }
}

Os intervalos ficam em torno de 1s, 2s, 4s e 8s, com um pedaço aleatório somado. Esse pedaço aleatório é o jitter, e ele existe para espalhar as tentativas de clientes diferentes em vez de sincronizar todas no mesmo segundo.

Quatro tentativas é um bom teto. Acima disso o usuário já foi embora.

Timeout de função

Toda função serverless tem tempo máximo de execução. Geração longa de texto passa fácil desse limite, e o resultado é uma função que morre depois de você já ter pago os tokens.

Três saídas, em ordem de esforço:

Streaming. A resposta começa a chegar em pedaços e a conexão fica ativa. Resolve a percepção de lentidão e evita o corte por inatividade.

Resposta menor. Limitar max_tokens ao que a tela realmente exibe corta tempo e custo ao mesmo tempo.

Fila. Para tarefa que leva minutos, a requisição do usuário não deve esperar a resposta.

Fila: o padrão que resolve a maioria dos casos

Em vez de o clique disparar a chamada cara, ele registra um pedido. Um worker processa depois.

create table jobs (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null,
  tipo text not null,
  entrada jsonb not null,
  status text not null default 'pendente',
  tentativas int not null default 0,
  resultado jsonb,
  erro text,
  criado_em timestamptz default now(),
  atualizado_em timestamptz default now()
);

create index on jobs (status, criado_em);

O fluxo fica assim: o clique insere uma linha com status pendente e devolve resposta imediata. Um cron chama a função de processamento a cada minuto. A função pega os pendentes mais antigos, processa e grava o resultado. O front consulta o status por polling ou realtime e atualiza a tela quando fica pronto.

Três detalhes que fazem a fila funcionar de verdade:

Marque como processando antes de chamar a API. Sem isso, duas execuções do cron pegam o mesmo job.

Conte as tentativas. Job que falhou três vezes vai para erro e para de consumir chamada.

Processe em lote pequeno. Pegar dez por rodada em vez de todos evita estourar o limite do provedor de uma vez.

Limite por usuário no seu lado

Rate limit do provedor protege o provedor. Quem protege a sua conta é o limite que você impõe.

// janela de 1 hora, 20 chamadas por usuário
const desde = new Date(Date.now() - 60 * 60 * 1000).toISOString();

const { count } = await supabaseAdmin
  .from('uso_ia')
  .select('id', { count: 'exact', head: true })
  .eq('user_id', userId)
  .gte('criado_em', desde);

if (count >= 20) {
  return new Response(
    JSON.stringify({ erro: 'limite_horario', tente_em_minutos: 60 }),
    { status: 429 }
  );
}

Devolver 429 do seu próprio backend permite ao front mostrar mensagem clara com o tempo de espera, em vez de erro genérico. A mesma tabela serve para cobrar por uso e para achar o usuário que está gerando custo desproporcional.

Custo como limite

Contagem de chamadas é aproximação. Quem tem prompt de tamanho variável faz melhor guardando tokens consumidos por chamada e limitando por soma no período. O campo usage da resposta traz os números de entrada e de saída, e gravar isso desde o primeiro dia evita a arqueologia de tentar descobrir depois quem gastou o quê.

Erros comuns

Erro Consequência Correção
Retry em erro 400 ou 401 Repete a falha e queima limite Só repetir 429, 5xx e 529
Retry sem espera crescente Piora a sobrecarga Backoff exponencial com jitter
Retry infinito Custo descontrolado e usuário travado Teto de tentativas com mensagem final
Chamada cara direto no clique Timeout no primeiro pico Fila com processamento assíncrono
Sem limite por usuário Um usuário gera a conta do mês Contagem em tabela com janela
Erro genérico na tela Usuário tenta de novo e piora Mensagem específica com tempo de espera

A ordem de implementação

Nem tudo precisa entrar no primeiro dia. A ordem que dá mais resultado por esforço:

Primeiro, retry com backoff. É código pequeno e resolve a maior parte das falhas transitórias.

Segundo, limite por usuário. Protege a conta antes de você precisar dela.

Terceiro, mensagem de erro decente. Barato e muda a percepção de qualidade.

Quarto, fila. Só quando existir tarefa que passe de alguns segundos.

Fazer os três primeiros leva uma tarde e cobre o cenário que derruba a maioria dos apps no primeiro pico de acesso.

Tags
#vibe coding#rate limit#retry#fila#api#produção
● Não perca essa chance

Não perca a próxima edição.

Toda quinta, 9h. Direto na sua caixa.

  • Ferramentas que economizam horas do seu trabalho
  • Agentes e automações que funcionam
  • Bastidores do que estamos construindo

100% gratuito. Cancele quando quiser.

Compartilhar