Blog

Developer tools: cómo gestionar rate limits en APIs de datos sin bloquear el pipeline

14 de agosto de 2026 · Equipo FeedScale

Developer tools: cómo gestionar rate limits en APIs de datos sin bloquear el pipeline

El equipo ha construido el pipeline. Las llamadas funcionan en staging. Llega el lunes de producción con carga real, y a las dos horas el sistema empieza a devolver 429 Too Many Requests. La ingesta se detiene. Los datos dejan de fluir. El equipo que consume esos datos al otro lado del pipeline no sabe por qué.

El rate limiting es uno de los problemas más previsibles del ecosistema de APIs de datos, y también uno de los más mal gestionados. No porque los developers no sepan que existe, sino porque en la mayoría de los proyectos se aborda tarde: cuando el problema ya está en producción, no durante el diseño.

Este post recorre los patrones técnicos que realmente funcionan cuando el volumen de señales que procesas es alto y el coste por llamada fallida no es cero.


Por qué el rate limiting rompe pipelines que "funcionaban bien"

El error clásico es dimensionar el pipeline contra el throughput medio, no contra el throughput pico. En staging, el volumen es controlado. En producción, hay picos: campañas de comunicación, eventos de mercado, crisis reputacionales que disparan el volumen de menciones en pocas horas.

Cuando el pipeline no tiene mecanismos de absorción de picos, las llamadas se acumulan, el proveedor devuelve 429, el sistema reintenta de forma inmediata (agravando el problema) y el pipeline queda bloqueado o, peor, empieza a descartar señales silenciosamente.

El segundo factor es la granularidad del límite. Muchas APIs de datos aplican límites por segundo, por minuto y por día simultáneamente. Un sistema que respeta el límite por minuto puede violar el límite por segundo si ráfagas cortas de llamadas no están distribuidas. El resultado es el mismo: 429, con un sistema que técnicamente "no ha superado la cuota".


Patrón 1: cola con presupuesto de llamadas explícito

El primer cambio estructural es separar la generación de solicitudes de la ejecución de llamadas API. En lugar de que cada proceso llame directamente al endpoint, las solicitudes van a una cola interna. Un componente dedicado —el dispatcher— consume la cola a la velocidad que el contrato API permite.

import time
import queue
import threading

class RateLimitedDispatcher:
    def __init__(self, calls_per_second: float, api_client):
        self.interval = 1.0 / calls_per_second
        self.queue = queue.Queue()
        self.client = api_client
        self._start_worker()

    def _start_worker(self):
        def worker():
            while True:
                request = self.queue.get()
                if request is None:
                    break
                self.client.execute(request)
                time.sleep(self.interval)
        threading.Thread(target=worker, daemon=True).start()

    def enqueue(self, request):
        self.queue.put(request)

Este patrón tiene una ventaja inmediata: el resto del sistema no necesita conocer los límites de la API. La lógica de control está centralizada. Cambiar el throughput permitido es un ajuste en un único punto.


Patrón 2: backoff exponencial con jitter real

El reintento inmediato tras un 429 es el error más común. Agrava exactamente el problema que lo provocó. El backoff exponencial es conocido; el jitter no siempre se implementa bien.

El jitter añade aleatoriedad al tiempo de espera para evitar que múltiples instancias del sistema reintenten de forma sincronizada —lo que recrea el pico original.

import random
import time

def backoff_with_jitter(attempt: int, base: float = 1.0, cap: float = 60.0) -> float:
    """Calcula el tiempo de espera con exponential backoff + full jitter."""
    exponential = min(cap, base * (2 ** attempt))
    return random.uniform(0, exponential)

def call_with_retry(api_func, max_attempts: int = 5):
    for attempt in range(max_attempts):
        try:
            return api_func()
        except RateLimitError:
            if attempt == max_attempts - 1:
                raise
            wait = backoff_with_jitter(attempt)
            time.sleep(wait)

El parámetro cap es importante: evita tiempos de espera absurdos en cascadas de errores. En APIs de datos con ventanas de límite por minuto, un cap de 60-90 segundos suele ser suficiente para esperar el reset de la ventana.


Patrón 3: circuit breaker para proteger el pipeline downstream

Cuando una API de datos falla de forma sostenida, el pipeline no debe seguir acumulando reintentos indefinidamente. Un circuit breaker detiene las llamadas durante un período de enfriamiento y las retoma de forma controlada.

import time
from enum import Enum

class State(Enum):
    CLOSED = "closed"       # Funcionando con normalidad
    OPEN = "open"           # Bloqueando llamadas
    HALF_OPEN = "half_open" # Probando recuperación

class CircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_timeout=30):
        self.threshold = failure_threshold
        self.timeout = recovery_timeout
        self.failures = 0
        self.state = State.CLOSED
        self.opened_at = None

    def call(self, func):
        if self.state == State.OPEN:
            if time.time() - self.opened_at > self.timeout:
                self.state = State.HALF_OPEN
            else:
                raise CircuitOpenError("Circuit abierto, llamada bloqueada")
        try:
            result = func()
            self._on_success()
            return result
        except Exception as e:
            self._on_failure()
            raise

    def _on_success(self):
        self.failures = 0
        self.state = State.CLOSED

    def _on_failure(self):
        self.failures += 1
        if self.failures >= self.threshold:
            self.state = State.OPEN
            self.opened_at = time.time()

El circuit breaker protege dos cosas: la API externa (evita seguir golpeándola cuando está bajo presión) y el pipeline interno (evita que los errores se propaguen hacia consumidores downstream que no tienen contexto del problema).


Monitorizar el presupuesto de llamadas antes de que se agote

Los tres patrones anteriores son reactivos: responden cuando el límite ya se ha alcanzado. La capa proactiva es monitorizar el consumo de cuota en tiempo real y ajustar el throughput antes de llegar al límite.

Las APIs bien diseñadas exponen la cuota restante en las cabeceras de respuesta: X-RateLimit-Remaining, X-RateLimit-Reset. No todas las APIs las incluyen, pero cuando están disponibles, leerlas es obligatorio.

def extract_rate_limit_headers(response_headers: dict) -> dict:
    return {
        "remaining": int(response_headers.get("X-RateLimit-Remaining", -1)),
        "reset_at": int(response_headers.get("X-RateLimit-Reset", 0)),
        "limit": int(response_headers.get("X-RateLimit-Limit", -1)),
    }

Con estos valores en mano, el dispatcher puede reducir su velocidad de forma anticipada cuando el porcentaje de cuota consumida supera un umbral (por ejemplo, 80%), sin esperar al 429.

En plataformas con modelo pay-as-you-go como FeedScale, este control tiene además un impacto directo en el coste: una llamada fallida que se reintenta tres veces puede contabilizar según el contrato. Gestionar el presupuesto de llamadas no es solo resiliencia técnica, es control de gasto.


Lo que el logging debe capturar siempre

Ninguno de estos patrones funciona si el sistema no genera trazabilidad suficiente. Cada llamada a la API debería registrar al menos:

Sin estos datos, depurar un pico de 429 a posteriori es casi imposible. El log solo dirá que hubo errores, no cuándo empezaron a acumularse ni qué componente los desencadenó.


El rate limiting como dato de diseño, no como caso de error

La diferencia entre un pipeline frágil y uno robusto no está en el código de negocio. Está en cómo se diseñó la relación con las APIs externas. El rate limiting no es un caso de error excepcional: es una restricción operativa conocida desde el primer día.

Tratarlo como tal —con presupuesto explícito, backoff con jitter, circuit breakers y monitorización de cabeceras— convierte un punto de fallo frecuente en un comportamiento predecible y gestionable. Eso es lo que distingue una integración de producción real de una prueba de concepto que funciona en staging.


← Volver al blog