Rate Limiting de API e Boas Práticas de Processamento em Lote — TongoRender Blog
Voltar ao Blog
guidesrate-limitingbatchperformance

Rate Limiting de API e Boas Práticas de Processamento em Lote

Domine rate limiting de API e processamento em lote: entenda limites de taxa, use endpoints de lote eficientemente, gerencie filas e implemente tratamento robusto de erros com retentativas.

TongoRender Team15 de janeiro de 20269 min

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.

Compartilhe este artigoCompartilhar no Twitter