Rate Limiting de API y Mejores Prácticas de Procesamiento por Lotes — TongoRender Blog
Volver al Blog
guidesrate-limitingbatchperformance

Rate Limiting de API y Mejores Prácticas de Procesamiento por Lotes

Domina el rate limiting de API y procesamiento por lotes: entiende los límites de tasa, usa endpoints de lote efectivamente, gestiona colas e implementa manejo robusto de errores con reintentos.

TongoRender Team15 de enero de 20269 min

Cuando tu aplicación genera PDFs o capturas de pantalla a escala, inevitablemente alcanzarás los límites de tasa de la API. Entender cómo funciona el rate limiting — y diseñar tu aplicación para manejarlo con gracia — es la diferencia entre un sistema de producción robusto y uno que falla bajo carga. Esta guía cubre conceptos de rate limiting, estrategias de procesamiento por lotes, gestión de colas y patrones de manejo de errores.

Entendiendo los Rate Limits

Los rate limits protegen tanto al proveedor de la API como a sus usuarios. Aseguran una asignación justa de recursos e impiden que un solo cliente sobrecargue el servicio:

  • Solicitudes por segundo (RPS) — ej: 10 solicitudes/segundo
  • Solicitudes por minuto (RPM) — ej: 100 solicitudes/minuto
  • Solicitudes concurrentes — ej: 5 solicitudes simultáneas
  • Cuota mensual — ej: 10,000 renderizaciones/mes

TongoRender retorna información de rate limit en los encabezados de respuesta:

X-RateLimit-Limit: 100        // Máximo de solicitudes por ventana
X-RateLimit-Remaining: 73     // Solicitudes restantes en la ventana actual
X-RateLimit-Reset: 1679529600 // Timestamp Unix cuando la ventana se reinicia
Retry-After: 30               // Segundos a esperar (solo en respuestas 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 alcanzado. Esperando ${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--;
    }
  }
}

Estrategias de Procesamiento por Lotes

Estrategia 1: Concurrencia 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(`Completados: ${succeeded.length}/${items.length}`);
  return { succeeded, failed };
}

Estrategia 2: Procesamiento en 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(`Procesando 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;
}

Estrategia 3: Procesamiento Basado en Cola

const { Queue, Worker } = require('bullmq');

const pdfQueue = new Queue('pdf-generation', { connection });

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);
}

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 } });

Manejo de Errores y Reintentos

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(`Intento ${attempt} falló. Reintentando en ${Math.round(delay)}ms...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

Resumen de Mejores Prácticas

  • Lee los encabezados de rate limit — No adivines los límites; usa los valores que la API te indica.
  • Implementa backoff exponencial — Nunca reintentes inmediatamente después de un error 429 o 500.
  • Agrega jitter a los retrasos — El jitter aleatorio previene problemas de thundering herd cuando múltiples workers reintentan simultáneamente.
  • Usa límites de concurrencia — Nunca dispares solicitudes paralelas ilimitadas.
  • Monitorea el uso — Sigue tu consumo diario y mensual para evitar agotamiento de cuota.
  • Falla con gracia — Almacena los elementos fallidos para reintento posterior en lugar de perderlos.
  • Usa webhooks — Para grandes lotes, prefiere procesamiento asíncrono con callbacks de webhook.

TongoRender proporciona límites de tasa generosos, encabezados de respuesta claros y un endpoint de lote para casos de uso de alto volumen. Siguiendo estos patrones, tu aplicación manejará cualquier volumen de manera confiable.

Escala con TongoRender — 100 renderizaciones gratuitas al mes, sin necesidad de tarjeta de crédito.

Comparte este artículoCompartir en Twitter