Introdução
Integrar APIs de IA em produção exige mais do que enviar prompts e tratar respostas. Erros de limite de requisição (429) e falhas temporárias no servidor (5xx) são comuns, especialmente sob carga variável. Sem uma estratégia de retry e controle de concorrência, sua aplicação pode derrubar a integração ou gerar custos desnecessários. Neste tutorial, mostramos como implementar backoff exponencial, filas e idempotência em Node.js usando a API da TokenRecarga.
Por que erros 429 e 5xx acontecem
- 429 Too Many Requests: você excedeu o limite de requisições por minuto (RPM) ou tokens por minuto (TPM) do modelo. Normalmente a resposta inclui o header
Retry-After, indicando quantos segundos esperar. - 5xx: erros do lado do servidor (502, 503, 504) são intermitentes e tendem a desaparecer após alguns segundos. Retentar é seguro, desde que com backoff.
Neste artigo, o endereço da API (BASE_URL) e a chave (API_KEY) ficam em variáveis de ambiente, fornecidas pela sua equipe de acesso.
Backoff exponencial com jitter
Backoff exponencial simples: aguarde delay = base * 2^attempt, mas adicione jitter aleatório para evitar que vários clientes retentem exatamente ao mesmo tempo. Sem jitter, picos sincronizados podem piorar o problema.
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function exponentialBackoff(attempt, baseDelayMs = 1000, maxDelayMs = 30000) {
const exp = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
// jitter entre 50% e 100% do valor calculado
const jitter = Math.random() * 0.5 + 0.5;
return Math.floor(exp * jitter);
}
Acima, usamos jitter de 50% a 100% para reduzir sincronização. Para uma estimativa conservadora, considere no máximo 5 tentativas com atraso inicial de 1 segundo.
Implementando retry com fetch
O fetch nativo do Node.js 18+ é suficiente. A função abaixo tenta a requisição, verificando se o status é retryable (429 ou >=500) e respeitando Retry-After, se presente.
const BASE_URL = process.env.BASE_URL;
const API_KEY = process.env.API_KEY;
async function retryableRequest(path, options = {}, { maxRetries = 5, baseDelayMs = 1000 } = {}) {
let attempt = 0;
while (attempt <= maxRetries) {
const response = await fetch(`${BASE_URL}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`,
...options.headers,
},
});
const retryable = response.status === 429 || response.status >= 500;
if (!retryable || attempt === maxRetries) {
return response;
}
const retryAfter = response.headers.get('retry-after');
let delayMs;
if (retryAfter) {
// Retry-After pode ser segundos ou data HTTP
delayMs = Number(retryAfter) * 1000;
if (Number.isNaN(delayMs)) {
delayMs = Math.max(0, new Date(retryAfter).getTime() - Date.now());
}
} else {
delayMs = exponentialBackoff(attempt, baseDelayMs);
}
console.warn(`Tentativa ${attempt + 1}/${maxRetries} falhou com status ${response.status}. Aguardando ${Math.round(delayMs / 1000)}s...`);
await sleep(delayMs);
attempt++;
}
}
Use retryableRequest para chamadas como /chat/completions. Se o status final não for 2xx, trate o erro adequadamente.
Filas para controlar concorrência
Mesmo com backoff, um alto número de requisições simultâneas pode estourar o rate limit. Uma fila com limite de concorrência ajuda a manter o throughput sob controle.
class SimpleQueue {
constructor(concurrency = 3) {
this.concurrency = concurrency;
this.running = 0;
this.queue = [];
}
enqueue(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this.process();
});
}
async process() {
if (this.running >= this.concurrency || this.queue.length === 0) {
return;
}
const { task, resolve, reject } = this.queue.shift();
this.running++;
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.running--;
this.process();
}
}
}
Use a fila para processos em lote, por exemplo:
const queue = new SimpleQueue(3); // no máximo 3 chamadas simultâneas
async function gerarResposta(prompt) {
return queue.enqueue(() =>
retryableRequest('/chat/completions', {
method: 'POST',
body: JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: prompt }] }),
})
);
}
Ajuste concurrency conforme seu limite de RPM. Os limites de cada modelo constam no seu contrato de acesso.
Idempotência para segurança em retries
Em retries, requisições POST podem ser executadas mais de uma vez se a primeira tentativa falhar após o servidor processar mas antes de enviar a resposta. Para evitar duplicidades, use chaves de idempotência. Muitas APIs aceitam o header Idempotency-Key; se a API da TokenRecarga suportar no endpoint usado, envie um UUID único por operação e repita-o em retries.
import { randomUUID } from 'node:crypto';
const idempotencyKey = randomUUID();
const response = await retryableRequest('/chat/completions', {
method: 'POST',
headers: {
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: 'Explique rate limit' }],
}),
});
Se o endpoint não suportar idempotência nativa, implemente uma camada local: armazene o hash do payload e a resposta correspondente; antes de chamar a API, verifique se já existe.
const cacheRespostas = new Map();
async function chamadaIdempotente(path, payload) {
const hash = crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
if (cacheRespostas.has(hash)) {
return cacheRespostas.get(hash);
}
const response = await retryableRequest(path, {
method: 'POST',
body: JSON.stringify(payload),
});
const data = await response.json();
cacheRespostas.set(hash, data);
return data;
}
Exemplo integrado
O código abaixo combina os três mecanismos: fila com concorrência 3, retry com backoff exponencial e idempotência local.
const queue = new SimpleQueue(3);
async function completarComIdempotencia(prompt) {
const payload = { model: 'gpt-4o-mini', messages: [{ role: 'user', content: prompt }] };
const hash = crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
if (!cacheRespostas.has(hash)) {
const task = async () => {
const response = await retryableRequest('/chat/completions', {
method: 'POST',
body: JSON.stringify(payload),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Erro ${response.status}: ${text}`);
}
return response.json();
};
cacheRespostas.set(hash, queue.enqueue(task));
}
return cacheRespostas.get(hash);
}
Conclusão
Tratar rate limits e erros 5xx exige uma combinação de backoff exponencial com jitter, controle de concorrência via filas e, quando possível, idempotência. Essas práticas evitam falhas em cascata e reduzem a necessidade de intervenção manual. Adapte os exemplos ao seu stack e monitore os logs para calibrar os limites. Siga o blog para outros tutoriais práticos.
Quer entender como a sua empresa pode usar modelos de IA com atendimento e faturamento no Brasil? Fale com a gente — a equipe da TokenRecarga responde em português.