Quando sua aplicação gera PDFs ou screenshots em escala, você inevitavelmente atingirá limites de taxa de API. Entender como o rate limiting funciona — e projetar sua aplicação para lidar com ele graciosamente — é a diferença entre um sistema de produção robusto e um que falha sob carga. Este guia cobre conceitos de rate limiting, estratégias de processamento em lote, gerenciamento de filas e padrões de tratamento de erros.
Entendendo Rate Limits
Rate limits protegem tanto o provedor da API quanto seus usuários. Eles garantem alocação justa de recursos e impedem que qualquer cliente sobrecarregue o serviço. Rate limits são tipicamente expressos como:
- Requisições por segundo (RPS) — ex: 10 requisições/segundo
- Requisições por minuto (RPM) — ex: 100 requisições/minuto
- Requisições concorrentes — ex: 5 requisições simultâneas
- Cota mensal — ex: 10.000 renderizações/mês
O TongoRender retorna informações de rate limit nos cabeçalhos de resposta:
X-RateLimit-Limit: 100 // Máximo de requisições por janela
X-RateLimit-Remaining: 73 // Requisições restantes na janela atual
X-RateLimit-Reset: 1679529600 // Timestamp Unix de quando a janela reseta
Retry-After: 30 // Segundos para aguardar (apenas em respostas 429)
Implementando Clientes Conscientes de Rate Limit
class RateLimitedClient {
constructor(apiKey, maxConcurrent = 5) {
this.apiKey = apiKey;
this.maxConcurrent = maxConcurrent;
this.activeRequests = 0;
this.remaining = Infinity;
this.resetTime = 0;
}
async request(endpoint, body) {
if (this.remaining <= 1) {
const waitTime = (this.resetTime * 1000) - Date.now();
if (waitTime > 0) {
console.log(`Rate limit atingido. Aguardando ${Math.ceil(waitTime / 1000)}s...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
while (this.activeRequests >= this.maxConcurrent) {
await new Promise(resolve => setTimeout(resolve, 100));
}
this.activeRequests++;
try {
const response = await fetch(`https://api.tongorender.io/v1/${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': this.apiKey },
body: JSON.stringify(body),
});
this.remaining = parseInt(response.headers.get('X-RateLimit-Remaining') || '100');
this.resetTime = parseInt(response.headers.get('X-RateLimit-Reset') || '0');
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '30');
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
return this.request(endpoint, body);
}
return response;
} finally {
this.activeRequests--;
}
}
}
Estratégias de Processamento em Lote
Estratégia 1: Concorrência Controlada
const pLimit = require('p-limit');
const limit = pLimit(5);
async function batchGenerate(items) {
const results = await Promise.allSettled(
items.map(item => limit(() => generatePDF(item)))
);
const succeeded = results.filter(r => r.status === 'fulfilled');
const failed = results.filter(r => r.status === 'rejected');
console.log(`Concluídos: ${succeeded.length}/${items.length}`);
return { succeeded, failed };
}
Estratégia 2: Processamento em Chunks
async function processInChunks(items, chunkSize = 10, delayMs = 2000) {
const results = [];
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
console.log(`Processando chunk ${Math.floor(i / chunkSize) + 1}`);
const chunkResults = await Promise.allSettled(chunk.map(item => generatePDF(item)));
results.push(...chunkResults);
if (i + chunkSize < items.length) {
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
return results;
}
Estratégia 3: Processamento Baseado em Fila
const { Queue, Worker } = require('bullmq');
const pdfQueue = new Queue('pdf-generation', { connection });
// Adicionar jobs à fila
async function queuePDFGeneration(items) {
const jobs = items.map(item => ({
name: 'generate-pdf',
data: item,
opts: { attempts: 3, backoff: { type: 'exponential', delay: 5000 } },
}));
await pdfQueue.addBulk(jobs);
}
// Processar jobs com concorrência controlada
const worker = new Worker('pdf-generation', async (job) => {
const pdf = await generatePDF(job.data);
await savePDF(pdf, job.data.id);
}, { connection, concurrency: 5, limiter: { max: 10, duration: 1000 } });
Tratamento de Erros e Retentativas
async function withRetry(fn, maxRetries = 3, baseDelay = 1000) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
const isRetryable = error.status === 429 || error.status >= 500;
if (!isRetryable || attempt === maxRetries) throw error;
const delay = baseDelay * Math.pow(2, attempt - 1) + Math.random() * 1000;
console.log(`Tentativa ${attempt} falhou. Retentando em ${Math.round(delay)}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Resumo de Boas Práticas
- Leia os cabeçalhos de rate limit — Não adivinhe limites; use os valores que a API informa.
- Implemente backoff exponencial — Nunca retente imediatamente após um erro 429 ou 500.
- Adicione jitter aos atrasos — Jitter aleatório previne problemas de thundering herd quando múltiplos workers retentam simultaneamente.
- Use limites de concorrência — Nunca dispare requisições paralelas ilimitadas.
- Monitore o uso — Acompanhe seu consumo diário e mensal para evitar esgotamento de cota.
- Falhe graciosamente — Armazene itens com falha para retentativa posterior em vez de perdê-los.
- Use webhooks — Para grandes lotes, prefira processamento assíncrono com callbacks de webhook.
O TongoRender fornece limites de taxa generosos, cabeçalhos de resposta claros e um endpoint de lote para casos de uso de alto volume. Seguindo esses padrões, sua aplicação lidará com qualquer volume de forma confiável.
Escale com o TongoRender — 100 renderizações gratuitas por mês, sem necessidade de cartão de crédito.