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 🙂 |