🔌 Backend & APIs 🛡️ Seguridad

Rate limiting y protección de APIs: patrones de producción

El rate limiting mal implementado da una falsa sensación de seguridad. Repasamos los algoritmos que se usan de verdad en producción, dónde aplicarlos, los headers estándar y los fallos que lo dejan sin efecto.

📅 8 de julio de 2026 ⏱️ 9 min de lectura ✍️ Equipo ProgramacionWebs

“Tenemos rate limiting” es una de esas frases que suenan a protección resuelta y con frecuencia no lo son. Es perfectamente posible tener un middleware de rate limiting activo, funcionando, generando métricas, y que un atacante lo esquive con un cambio de dos líneas en sus peticiones. El rate limiting no es una casilla que se marca una vez: es una combinación de dónde lo aplicas, qué algoritmo usas, por qué clave identificas al cliente y qué haces cuando el propio limitador falla. Este artículo repasa esas cuatro decisiones con la profundidad que rara vez recibe en la documentación de una librería.

Si todavía estás definiendo el contrato general de tu API, esto complementa a Diseño de APIs REST en 2026: buenas prácticas que de verdad importan; si el foco es la superficie de seguridad completa de la API, el rate limiting es una de las categorías que cubre OWASP Top 10 2026: qué ha cambiado y cómo protegerte.

Los algoritmos que se usan de verdad en producción

Hay cuatro enfoques clásicos, y elegir mal entre ellos es la primera fuente de problemas.

Fixed window cuenta peticiones en bloques de tiempo fijos (por ejemplo, “100 peticiones por minuto natural”). Es trivial de implementar con un contador que se resetea, pero tiene un defecto conocido: un cliente puede agotar su cupo en el último segundo de una ventana y otro cupo completo en el primer segundo de la siguiente, logrando el doble del límite nominal en un intervalo de dos segundos.

Sliding window corrige ese problema promediando el conteo entre la ventana actual y la anterior de forma proporcional al tiempo transcurrido, suavizando los picos en los bordes. Cloudflare, que ejecuta esta variante a escala global, reporta una tasa de error de en torno al 0,003% sobre cientos de millones de peticiones: no es matemáticamente perfecto, pero es lo bastante preciso para la inmensa mayoría de casos de uso.

Token bucket es el que mejor entiende el tráfico real de una aplicación orientada a usuario: cada cliente tiene un cubo con capacidad máxima de tokens, que se rellena a un ritmo constante, y cada petición consume un token. La ventaja frente a las ventanas fijas es que permite ráfagas controladas —una carga de página que dispara ocho peticiones simultáneas y luego queda en silencio— sin penalizar al cliente por comportarse de forma normal.

GCRA (Generic Cell Rate Algorithm), heredado de las redes ATM, es una variante del leaky bucket que en vez de mantener un contador guarda un único timestamp por cliente (el theoretical arrival time, o TAT): cada petición calcula si el tiempo transcurrido desde el TAT es suficiente según la tasa configurada, y si lo es, actualiza el TAT sumándole el intervalo de emisión. Es exacto matemáticamente, sin aproximaciones, y extremadamente barato de ejecutar de forma atómica en Redis con un único SET por clave, lo que explica por qué Stripe y Shopify lo usan como base de sus limitadores distribuidos.

-- Esbozo simplificado de GCRA en Redis (vía script Lua para atomicidad)
local tat = tonumber(redis.call("GET", KEYS[1])) or now
local allow_at = math.max(tat, now)
if allow_at - now <= burst_tolerance then
  redis.call("SET", KEYS[1], allow_at + emission_interval, "EX", ttl)
  return 1 -- permitido
else
  return 0 -- rechazado
end

Dónde aplicarlo: edge, gateway y aplicación no son excluyentes

Una pregunta que se responde mal con frecuencia: “¿el rate limiting va en el CDN o en el código?”. La respuesta de producción casi siempre es “en varios sitios a la vez”, cada uno con un propósito distinto.

graph LR
A[Cliente] --> B[Edge / CDN]
B -->|límite grueso por IP, DDoS| C[API Gateway]
C -->|límite por API key / plan| D[Aplicación]
D -->|límite fino por usuario y endpoint| E[Base de datos / recursos]
  • Edge / CDN (Cloudflare, Fastly): la primera línea de defensa, pensada para volumen y para parar tráfico abusivo antes de que consuma ancho de banda o CPU de tu origen. Cloudflare, por ejemplo, cuenta peticiones con alcance por centro de datos, no global: cada nodo de sus más de 335 ubicaciones aplica su propio contador, lo que compra resiliencia y velocidad a cambio de algo de imprecisión frente a un límite estrictamente global. También advierte explícitamente que puede haber un margen de unos segundos entre detectar el exceso y aplicar la mitigación, durante el cual algunas peticiones de más sí llegan al origen.
  • API Gateway: el lugar natural para límites diferenciados por API key, plan de suscripción o cliente autenticado, porque ya tiene el contexto de autenticación resuelto antes de reenviar la petición al backend.
  • Aplicación: el único sitio donde tienes contexto de negocio real —un usuario autenticado, el coste real de una operación concreta, reglas específicas de un endpoint sensible como login o creación de recursos costosos—. Un límite genérico en el edge nunca sustituye a un límite específico de “5 intentos de login por cuenta cada 15 minutos”.

Ninguna de las tres capas sustituye a las otras. El edge protege tu infraestructura de volumen bruto; el gateway diferencia por cliente; la aplicación protege reglas de negocio concretas. Quitar cualquiera de las tres deja un hueco real, no redundante.

Los headers estándar: qué comunicar al cliente

Durante años cada API inventó su propio formato (X-RateLimit-Limit, X-RateLimit-Remaining, cada una con semántica ligeramente distinta). El grupo de trabajo HTTPAPI de IETF trabaja desde hace tiempo en estandarizar esto con los campos RateLimit y RateLimit-Policy, ya en su versión de borrador 11 (mayo de 2026), usando la sintaxis de structured fields de RFC 9651:

RateLimit-Policy: "burst";q=100;w=60, "daily";q=1000;w=86400
RateLimit: "default";r=42;t=38

RateLimit-Policy anuncia la política del servidor (cupo q, ventana w en segundos); RateLimit comunica el estado actual (cuota restante r, segundos hasta que se repone t). La especificación sigue siendo un Internet-Draft, no una RFC aprobada —una revisión de directorate en 2026 la devolvió marcada como “no lista” y la sintaxis puede cambiar todavía—, así que no es (aún) el estándar universal, pero sí la dirección hacia la que camina el ecosistema tras años de headers X-* no oficiales e inconsistentes entre proveedores.

Lo que sí es un estándar estable, y no debería faltar en ninguna respuesta 429, es el header Retry-After definido en el propio HTTP: indica en segundos (o como fecha) cuándo tiene sentido que el cliente reintente. Un cliente bien construido lo respeta antes de reintentar a ciegas; no incluirlo convierte cada 429 en una adivinanza para quien consume tu API.

HTTP/1.1 429 Too Many Requests
Retry-After: 30
RateLimit: "default";r=0;t=30
Content-Type: application/problem+json

{
  "type": "https://api.miempresa.com/errors/rate-limited",
  "title": "Demasiadas peticiones",
  "status": 429
}

Errores comunes que dejan una API “protegida” pero vulnerable

Aquí está la parte que más importa, porque son los fallos que un equipo descubre solo cuando alguien los explota.

Confiar en X-Forwarded-For sin validar el proxy. Si tu aplicación identifica al cliente por la IP que llega en esa cabecera pero no está configurada para confiar únicamente en proxies conocidos, un atacante puede rotar valores falsos en cada petición (X-Forwarded-For: 1.2.3.4, luego 1.2.3.5, etc.) y generar un contador nuevo en cada intento, evitando el límite por completo. Este no es un problema teórico: se ha reportado como vulnerabilidad de seguridad real en frameworks de producción, precisamente porque la configuración por defecto de “cuántos saltos de proxy confiar” se deja mal ajustada.

Contadores en memoria local en un despliegue con múltiples instancias. Si cada réplica de tu servicio lleva su propio contador en memoria, un cliente que reparte sus peticiones entre las N instancias detrás del balanceador obtiene, de facto, N veces el límite configurado. El contador tiene que vivir en un almacén compartido (Redis es la opción estándar) para que el límite sea real en un entorno horizontal.

Limitar solo por IP cuando el endpoint tiene autenticación. Una IP residencial puede representar a cientos de usuarios detrás de un NAT compartido, y una IP de un proveedor cloud puede representar a uno solo con cientos de cuentas. Para endpoints autenticados, la clave correcta es el identificador de usuario o de API key; la IP es, como mucho, un límite secundario. Para endpoints sensibles sin autenticación previa —login, registro, recuperación de contraseña— la práctica robusta es limitar por ambos ejes a la vez: por IP y por la cuenta objetivo, porque cada uno detiene un ataque distinto (fuerza bruta distribuida contra una cuenta, o spraying de credenciales contra muchas cuentas desde un mismo origen).

No limitar por coste real de la operación. Tratar igual un GET /users/42 que un POST /reports/export que dispara una consulta agregada de varios segundos es un error de diseño, no solo de configuración: un atacante no necesita saturar tu límite de peticiones por segundo si puede saturar tu base de datos con unas pocas peticiones caras. Los límites deberían poder variar por endpoint, no ser un número global aplicado uniformemente a toda la API.

Mensajes de error que filtran el propio límite exacto y el momento óptimo para reintentar sin fricción añadida. Esto es menos crítico, pero real: un 429 sin Retry-After empuja a los clientes mal construidos a reintentar inmediatamente en bucle, generando más carga, no menos.

Recomendaciones de producción

Un rate limiting que sostiene un ataque real, no solo una demo, combina varias piezas: contador compartido (Redis) en lugar de memoria local por instancia; token bucket o GCRA como algoritmo por defecto para tráfico de usuario; límites diferenciados por endpoint según su coste real, no un número plano para toda la API; clave de identificación correcta según el contexto (usuario autenticado primero, IP como respaldo, ambos en endpoints sensibles sin autenticar); validación explícita de qué proxies son de confianza antes de leer X-Forwarded-For; y una capa de edge delante de todo para el volumen bruto que ni siquiera debería llegar a tu aplicación. Ningún componente individual de esta lista sustituye a los demás: el rate limiting robusto es la suma, no la mejor pieza aislada.

Compartir