Javier Valencia Javier Valencia
Optimización de Nginx para WordPress: FastCGI Cache y Rate Limiting

Optimización de Nginx para WordPress: FastCGI Cache y Rate Limiting

Javier Valencia · · 3 min de lectura · 3484 visitas · DevOps
nginx devops wordpress cache performance

Si tienes un WordPress corriendo sobre Nginx + PHP-FPM, estas dos configuraciones van a mejorar drásticamente el rendimiento y la seguridad de tu sitio: FastCGI Cache para servir páginas a velocidad de vértigo y Rate Limiting para proteger las rutas más atacadas.

Todo esto asume que ya tienes Nginx sirviendo WordPress a través de PHP 8.4 mediante un socket Unix (php8.4-fpm.sock).

FastCGI Cache: sirve WordPress sin tocar PHP

Optimización de Nginx para WordPress: FastCGI Cache y Rate Limiting

La idea es simple: cuando un visitante anónimo solicita una página, Nginx la procesa una vez a través de PHP-FPM y guarda el resultado en disco. Las siguientes visitas a esa misma URL se sirven directamente desde la caché de Nginx, sin que PHP ni la base de datos se enteren.

Definir la zona de caché

En el bloque http {} de tu nginx.conf:

fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2
                   keys_zone=WORDPRESS:100m
                   inactive=60m
                   max_size=512m
                   use_temp_path=off;

fastcgi_cache_key "$scheme$request_method$host$request_uri";

fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
  • keys_zone=WORDPRESS:100m: reserva 100 MB en memoria para las claves de caché. Suficiente para millones de entradas.
  • inactive=60m: si una entrada no se accede en 60 minutos, se elimina.
  • max_size=512m: tamaño máximo en disco. Ajústalo según tu espacio disponible.
  • use_temp_path=off: escribe directamente en el directorio de caché, evitando una copia intermedia.
  • fastcgi_ignore_headers: PHP y WordPress envían headers que invalidarían la caché innecesariamente. Los ignoramos.

Decidir qué NO cachear

No todo debe cachearse. Los usuarios logueados, el panel de administración, las búsquedas y las páginas de WooCommerce (carrito, checkout, mi cuenta) deben llegar siempre a PHP:

set $skip_cache 0;

# Peticiones POST
if ($request_method = POST) { set $skip_cache 1; }

# URLs con query strings
if ($query_string != "") { set $skip_cache 1; }

# Rutas de administración y dinámicas
if ($request_uri ~* "/wp-admin/|/wp-login.php|/xmlrpc.php|/wp-cron.php|/cart/|/checkout/|/my-account/") {
    set $skip_cache 1;
}

# Cookies de usuario logueado o carrito
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in|woocommerce_cart_hash|woocommerce_items_in_cart") {
    set $skip_cache 1;
}

Aplicar la caché en el bloque PHP

location ~ \.php$ {
    try_files $uri =404;

    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

    # Caché
    fastcgi_cache WORDPRESS;
    fastcgi_cache_valid 200 301 302 60m;
    fastcgi_cache_valid 404 1m;
    fastcgi_cache_bypass $skip_cache;
    fastcgi_no_cache $skip_cache;

    # Resiliencia
    fastcgi_cache_use_stale error timeout updating invalid_header http_500 http_503;
    fastcgi_cache_lock on;
    fastcgi_cache_lock_timeout 5s;
    fastcgi_cache_background_update on;

    # Header de debug
    add_header X-FastCGI-Cache $upstream_cache_status;
}

Tres directivas clave aquí:

  • fastcgi_cache_use_stale ... updating: si PHP-FPM falla o la caché está refrescándose, Nginx sirve la versión anterior en lugar de devolver un error. Tu sitio se vuelve prácticamente indestructible ante picos de tráfico.
  • fastcgi_cache_lock on: ante un cache MISS con múltiples peticiones simultáneas a la misma URL, solo una pasa a PHP-FPM. El resto espera el resultado. Evita la «estampida» de requests idénticos.
  • fastcgi_cache_background_update on: cuando una entrada está a punto de expirar, Nginx la refresca en segundo plano mientras sigue sirviendo la versión actual.

Archivos estáticos

Los archivos estáticos no deben pasar por PHP. Sírvelos directamente con cabeceras de caché agresivas:

location ~* \.(js|css|png|jpg|jpeg|gif|webp|avif|ico|svg|woff2?|ttf|eot|otf)$ {
    expires 365d;
    access_log off;
    add_header Cache-Control "public, immutable";
    try_files $uri =404;
}

Buffers y compresión

# Buffers FastCGI
fastcgi_buffer_size 32k;
fastcgi_buffers 16 16k;
fastcgi_busy_buffers_size 32k;

# Gzip
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types text/plain text/css application/json application/javascript
           text/xml application/xml application/xml+rss text/javascript
           image/svg+xml application/x-font-ttf font/opentype;

Crear el directorio y verificar

mkdir -p /var/cache/nginx/fastcgi
chown www-data:www-data /var/cache/nginx/fastcgi
nginx -t && systemctl reload nginx

Para comprobar que funciona:

curl -I https://tudominio.com | grep X-FastCGI-Cache

La primera visita mostrará MISS, la segunda HIT. Si ves BYPASS, es que la petición cumple alguna de las reglas de exclusión (usuario logueado, POST, etc.).

Rate Limiting: proteger las rutas sensibles

WordPress tiene varias rutas que son objetivo constante de ataques de fuerza bruta y abuso. Vamos a limitar cuántas peticiones por segundo puede hacer una misma IP a cada una.

Definir las zonas

En el bloque http {}:

limit_req_zone $binary_remote_addr zone=login:10m rate=3r/m;
limit_req_zone $binary_remote_addr zone=xmlrpc:10m rate=1r/m;
limit_req_zone $binary_remote_addr zone=admin:10m rate=10r/s;

limit_req_log_level warn;

Cada zona usa $binary_remote_addr (la IP del cliente en formato binario, ocupa solo 16 bytes para IPv6) como clave y reserva 10 MB de memoria compartida.

Aplicar los límites

# wp-login.php — 3 peticiones por minuto
location = /wp-login.php {
    limit_req zone=login burst=5 nodelay;
    limit_req_status 429;

    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

# xmlrpc.php — 1 petición por minuto (o bloquearlo directamente)
location = /xmlrpc.php {
    # Opción A: rate limit
    limit_req zone=xmlrpc burst=2 nodelay;
    limit_req_status 429;

    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

    # Opción B: bloquearlo del todo
    # deny all;
    # return 444;
}

# wp-admin — más permisivo para uso normal
location /wp-admin/ {
    limit_req zone=admin burst=20 nodelay;
    limit_req_status 429;

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.4-fpm.sock;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

# wp-cron.php
location = /wp-cron.php {
    limit_req zone=xmlrpc burst=2 nodelay;
    limit_req_status 429;

    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

Tabla resumen de límites

Ruta Rate Burst Motivo
wp-login.php 3/min 5 Protección contra fuerza bruta
xmlrpc.php 1/min 2 Vector de ataque habitual
wp-admin/ 10/s 20 Permite uso normal, frena abuso
wp-cron.php 1/min 2 Evitar abuso de cron externo

El parámetro burst define cuántas peticiones extra se permiten por encima del rate antes de empezar a rechazar. Con nodelay, esas peticiones extra del burst se procesan inmediatamente en lugar de encolarse.

Cuando una IP supera el límite, recibe un HTTP 429 Too Many Requests.

Monitorizar los bloqueos

tail -f /var/log/nginx/error.log | grep "limiting"

Nota sobre xmlrpc.php

Si no usas aplicaciones externas que se conecten a tu WordPress mediante XML-RPC (la app móvil de WordPress, Jetpack, pingbacks…), bloquéalo directamente con return 444. Es el vector de ataque más explotado en WordPress, usado tanto para fuerza bruta como para ataques de amplificación DDoS.

Resultado

Optimización de Nginx para WordPress: FastCGI Cache y Rate Limiting

Con estas dos configuraciones:

  • Los visitantes anónimos reciben las páginas directamente desde la caché de Nginx, sin que PHP ni MySQL intervengan. El tiempo de respuesta pasa de cientos de milisegundos a unos pocos.
  • Los picos de tráfico se absorben sin problemas gracias a fastcgi_cache_lock y fastcgi_cache_use_stale.
  • Los ataques de fuerza bruta contra wp-login.php y xmlrpc.php quedan limitados a unas pocas peticiones por minuto por IP.
  • PHP-FPM se dedica solo a lo que realmente necesita procesamiento dinámico: el panel de administración, usuarios logueados y formularios.