Contents
Introducción
En entornos con tráfico medio o alto, limitar la tasa de peticiones a los endpoints REST de WordPress es una práctica esencial para proteger la API frente a abusos (bots, scrapers, ataques de fuerza bruta) y para garantizar la estabilidad del servidor. En este artículo se explica en detalle cómo diseñar e implementar rate limiting en PHP dentro de WordPress: estrategias, dónde integrarlo, ejemplos de código prácticos (desde una solución básica con transients hasta un token-bucket más avanzado usando caché persistente), consideraciones de implementación y buenas prácticas operativas.
Por qué aplicar rate limiting
- Protección de recursos: evita picos de CPU, memoria y consultas DB por solicitudes repetidas.
- Calidad de servicio: mantiene la disponibilidad para usuarios legítimos ante un volumen elevado de peticiones.
- Control de abuso: frena scrapers y bots que consumen ancho de banda y contenido.
- Métricas y seguridad: facilita detectar y registrar comportamientos anómalos.
Estrategias comunes de rate limiting
- Fixed window counter: contador por intervalo fijo (por ejemplo, 100 peticiones por minuto). Simple y eficiente pero puede permitir ráfagas al final/inicio de ventanas.
- Sliding window (aproximado): suaviza el efecto de las ventanas fijas, más preciso que el fixed window simple.
- Sliding window log: almacena marcas de tiempo (timestamps) de peticiones muy preciso pero costoso en almacenamiento/CPU.
- Token bucket (recomendado): permite ráfagas hasta una capacidad y rellena tokens a una tasa constante flexible y eficiente si se implementa con una caché atómica rápido (Redis, Memcached).
- Leaky bucket: parecido a token bucket pero con comportamiento de goteo constante útil para suavizar picos.
Dónde integrar el rate limiting en WordPress
- permission_callback al registrar rutas: al usar register_rest_route, la clave permission_callback permite devolver WP_Error para denegar la petición antes del callback principal es ideal para comprobaciones por petición.
- hook rest_pre_dispatch / rest_pre_serve_request: permiten interceptar solicitudes de forma global (útil si quieres aplicar reglas a todas o muchas rutas sin tocar cada registro).
- middleware propio: crear un plugin que registre comprobaciones globales y añada cabeceras (X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After).
Implementación básica (Fixed Window) usando transients
Este ejemplo aplica un límite por IP y ruta, con ventana fija (por ejemplo, 60 peticiones por minuto). Se registra la ruta y se usa permission_callback para validar el límite. Se emiten cabeceras de control y se devuelve WP_Error con status 429 cuando se excede.
GET,
callback => mi_plugin_recurso_callback,
permission_callback => mi_plugin_recurso_rate_limit_permission,
) )
} )
function mi_plugin_get_client_ip() {
// Manejar proxys comunes (Cloudflare, Nginx, etc). Ajustar según infra.
if ( ! empty( _SERVER[HTTP_X_FORWARDED_FOR] ) ) {
ips = explode( ,, _SERVER[HTTP_X_FORWARDED_FOR] )
return trim( ips[0] )
}
return isset( _SERVER[REMOTE_ADDR] ) ? _SERVER[REMOTE_ADDR] : 0.0.0.0
}
function mi_plugin_recurso_rate_limit_permission( request ) {
limit = 60 // peticiones permitidas por ventana
window = 60 // ventana en segundos (1 minuto)
route = request->get_route() // /mi-plugin/v1/recurso (o similar)
ip = mi_plugin_get_client_ip()
// Clave por IP ruta ventana actual
window_index = floor( time() / window )
key = rl_ . md5( route . . ip . . window_index )
count = get_transient( key )
if ( false === count ) {
count = 0
}
if ( count >= limit ) {
retry_after = window - ( time() % window )
header( Retry-After: . retry_after )
header( X-RateLimit-Limit: . limit )
header( X-RateLimit-Remaining: 0 )
return new WP_Error( rest_rate_limited, Demasiadas peticiones, array( status => 429 ) )
}
count
// Expirar al final de la ventana actual para sincronizar contador
expires = window - ( time() % window )
set_transient( key, count, expires )
header( X-RateLimit-Limit: . limit )
header( X-RateLimit-Remaining: . max( 0, limit - count ) )
return true
}
function mi_plugin_recurso_callback( request ) {
data = array( mensaje => Respuesta del endpoint con rate limiting activo )
return rest_ensure_response( data )
}
?>
Notas sobre el ejemplo básico
- Se usa get_transient / set_transient por simplicidad. En instalaciones con object cache persistente (Redis, Memcached) estos transients pueden mapear a la caché y funcionar bien.
- El método de ventana fija (fixed window) es simple y suficientemente eficiente para muchos casos, pero permite ráfagas cuando la ventana rota. Si necesitas suavizar ráfagas, considera token bucket o sliding window.
- Se añaden cabeceras HTTP útiles para los clientes: X-RateLimit-Limit, X-RateLimit-Remaining y Retry-After en caso de bloqueo.
- La función mi_plugin_get_client_ip es básica en entornos detrás de CDN/proxy deberás confiar en cabeceras que tu infraestructura garante (por ejemplo, CF-Connecting-IP para Cloudflare) y asegurarte de que el proxy es confiable para evitar spoofing de IP.
Implementación avanzada: Token Bucket usando caché persistente
Token bucket ofrece control de tasa con tolerancia a ráfagas. Se recomienda usar una caché rápida y con operaciones atómicas (idealmente Redis con LUA o Memcached con incr/decr atómicos). A continuación un ejemplo general usando wp_cache_get/wp_cache_set si tu backend soporta operaciones atómicas, adapta para usarlas (cache_incr, INCRBY, o script LUA para Redis).
GET,
callback => mi_plugin_recurso_avanzado_callback,
permission_callback => mi_plugin_recurso_avanzado_rate_limit,
) )
} )
function mi_plugin_recurso_avanzado_rate_limit( request ) {
capacity = 10 // tokens máximos (burst)
rate_per_sec = 0.5 // tokens añadidos por segundo (ej: 0.5 => 30 tokens/minuto)
identifier = // por defecto IP, si está autenticado usar user ID
user_id = get_current_user_id()
if ( user_id ) {
identifier = user: . user_id
} else {
identifier = ip: . mi_plugin_get_client_ip()
}
key = rl_tb_ . md5( identifier )
now = microtime( true )
// Estado guardado: [tokens => float, last => float]
state = wp_cache_get( key, rate_limit )
if ( false === state ! is_array( state ) ) {
state = array( tokens => capacity, last => now )
}
// Refill tokens
elapsed = max( 0, now - state[last] )
added = elapsed rate_per_sec
state[tokens] = min( capacity, state[tokens] added )
state[last] = now
if ( state[tokens] >= 1 ) {
// Consumir un token y permitir
state[tokens] -= 1
wp_cache_set( key, state, rate_limit, 3600 ) // TTL suficientemente largo, el last mantiene la info
remaining = floor( state[tokens] )
header( X-RateLimit-Limit: . capacity )
header( X-RateLimit-Remaining: . remaining )
return true
} else {
// Sin tokens: denegar. Informar un Retry-After aproximado
needed = 1 - state[tokens]
retry_after = ceil( needed / rate_per_sec )
header( Retry-After: . retry_after )
header( X-RateLimit-Limit: . capacity )
header( X-RateLimit-Remaining: 0 )
return new WP_Error( rest_rate_limited, Demasiadas peticiones, array( status => 429 ) )
}
}
function mi_plugin_recurso_avanzado_callback( request ) {
return rest_ensure_response( array( mensaje => Recurso avanzado con token bucket ) )
}
?>
Comentarios sobre token bucket
- Si hay múltiples procesos PHP concurrentes, wp_cache_get/wp_cache_set sin soporte de operaciones atómicas puede provocar condiciones de carrera. Para producción, use Redis con un script LUA atómico o Memcached con incrementos atómicos.
- El parámetro capacity define la capacidad de ráfaga, rate_per_sec define la tasa sostenida.
- Guarde por usuario autenticado (user ID) para no penalizar a múltiples usuarios detrás de la misma IP (por ejemplo, en NAT o en redes corporativas).
Recomendaciones operativas y consideraciones prácticas
- Almacenamiento: Para bajo tráfico, transients (get_transient/set_transient) son suficientes. Para tráfico medio/alto use Redis/Memcached o una tabla SQL optimizada.
- Atomicidad: Evite condiciones de carrera mediante operaciones atómicas de la caché (INCR/DECR) o scripts LUA en Redis. Las soluciones no atómicas pueden subestimar la utilización y permitir más peticiones de las deseadas.
- Identificación de cliente: Preferible por user ID cuando la petición está autenticada por IP cuando es anónima. Si la infraestructura incluye CDN o proxies, utilice cabeceras fiables (configuradas por el proxy) y valide su origen.
- Excepciones: Implemente listas blancas para servicios internos, webhooks de confianza o IPs de monitoreo para evitar bloquear integraciones legítimas.
- Métodos y rutas: Tal vez no quieras aplicar las mismas reglas a todos los endpoints (por ejemplo, endpoints de búsqueda pública vs endpoints administrativos). Configure políticas por ruta o por grupo de rutas.
- Cabeceras estándar: Devuelva X-RateLimit-Limit, X-RateLimit-Remaining y Retry-After para que los clientes sepan su estado y cuándo reintentar.
- Logging y métricas: Registre intentos bloqueados, picos y patrones para mejorar reglas y detectar abuso. Integre con Prometheus, Elastic o similar si lo requiere.
- Pruebas: Simule carga con herramientas (curl, ab, wrk) desde direcciones diferentes y desde la misma IP para validar comportamiento y latencias introducidas.
Problemas comunes y cómo mitigarlos
- Usuarios legítimos bloqueados por NAT: identifica usuarios autenticados y aplícales límites por usuario en lugar de por IP.
- Cabeceras spoofeadas: asegúrate de confiar sólo en cabeceras (X-Forwarded-For, etc.) si provienen de proxies/infraestructura controlada.
- Rendimiento: la verificación de rate limiting debe ser rápida y con latencia mínima evitar operaciones DB pesadas en cada petición.
- Bloqueos en cascada: si tu limitador es demasiado agresivo puede causar más carga (clientes reintentando). Devuelve Retry-After para reducir reintentos inmediatos.
Pruebas y validación
- Prueba localmente con scripts que hagan múltiples peticiones por segundo y observa X-RateLimit-Remaining y 429.
- Introduce logs temporales (error_log o sistema de logging) para obtener trazabilidad durante la puesta en marcha.
- Valida la precisión temporal de las ventanas y el comportamiento con ráfagas cortas y sostenidas.
Resumen
Aplicar rate limiting en endpoints REST de WordPress protege tu API, mejora la disponibilidad y ayuda a controlar abusos. Para comenzar, una solución sencilla basada en transients con ventanas fijas puede ser suficiente para tráfico real y concurrencia, adopta token buckets apoyados en caché persistente (Redis/Memcached) con operaciones atómicas. Sea cual sea el método, asegúrate de incluir cabeceras informativas, lógica diferenciada para usuarios autenticados y anónimos, y métricas/registro para ajustar políticas.
Recuerda
Implementa primero una política conservadora en un entorno de pruebas, monitoriza el impacto y ajusta parámetros (limit, window, capacity, rate) según comportamiento real y necesidades de tu servicio.
|
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |
