Como aplicar rate limiting a endpoints REST en PHP en WordPress

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

  1. 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.
  2. 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.
  3. 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.
  4. Excepciones: Implemente listas blancas para servicios internos, webhooks de confianza o IPs de monitoreo para evitar bloquear integraciones legítimas.
  5. 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.
  6. Cabeceras estándar: Devuelva X-RateLimit-Limit, X-RateLimit-Remaining y Retry-After para que los clientes sepan su estado y cuándo reintentar.
  7. 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.
  8. 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 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *