Escalar WordPress: cómo soportar 10.000 usuarios por hora

Escalar WordPress: cómo soportar 10.000 usuarios por hora

Última verificación: 18 de octubre de 2026
16 min de lectura
Guía
Core Web Vitals

Todo propietario de un sitio sueña con hacerse viral: el momento en que su artículo llega a Reddit, a Wykop (el agregador de enlaces polaco) o a la portada de Onet.

Para el administrador del servidor, ese momento a menudo se convierte en una pesadilla. La página empieza a tardar 30 segundos en cargar y después los visitantes solo ven “Error 500: Internal Server Error” o “503 Service Unavailable”.

Este artículo se basa en nuestra charla en una Fuckup Night. Muestra cómo preparar WordPress para mucho tráfico y por qué un plugin de caché por sí solo no basta.


#Cómo limitan los PHP workers el tráfico de WordPress

La mayoría de las caídas de WordPress no se deben a la falta de RAM o de CPU, sino a que se agota el límite de PHP workers.

#Qué es un PHP worker

Imagina un PHP worker como un cajero de una tienda.

  • Tienes 10 cajeros (10 workers).
  • Cada cliente (visitante) necesita 1 segundo para ser atendido (el tiempo de generación de la página).
  • Puedes atender como máximo a 10 clientes por segundo.

Si llega el cliente número 11, espera en la cola. Si la cola se llena, el servidor devuelve un error 503.

En PHP-FPM, ese “cajero” es un proceso hijo del pool. La documentación de PHP describe la directiva pm.max_children sin rodeos: fija el límite de peticiones atendidas a la vez (php.net, configuración de FPM). Las peticiones por encima de ese límite esperan en la cola del socket, cuya longitud define listen.backlog (511 por defecto en Linux).

#Cómo calcular la capacidad de los PHP workers

Tráfico máximo (RPS) = número de workers / tiempo medio de generación de la página

Un cálculo de ejemplo para 10 workers en un alojamiento compartido:

  • La página se genera en 0,5 s, así que puedes aceptar 20 peticiones por segundo.
  • La página se genera en 2,0 s, así que puedes aceptar solo 5 peticiones por segundo.

La misma fórmula muestra qué significan los 10.000 usuarios por hora del título. Son de media menos de 3 páginas vistas por segundo, una cifra que incluso 10 workers soportan a 0,5 s por página. El problema empieza cuando el tráfico no se reparte por igual, sino que llega en pocos minutos tras publicarse un enlace, o cuando una sola página vista lanza varias peticiones PHP (AJAX, API REST, carrito).

Conclusión: para escalar, o contratas más cajeros (servidores VPS o dedicados más caros) o haces que los cajeros trabajen más rápido (optimización de código y caché).


#Cómo configurar pm.max_children en PHP-FPM

El número de workers se define en el archivo del pool de PHP-FPM (normalmente www.conf). La directiva pm admite tres valores (php.net):

Modo pmCómo funcionaCuándo tiene sentido
staticun número fijo de procesos igual a pm.max_childrenservidor dedicado con tráfico previsible
dynamicel número de procesos varía entre pm.min_spare_servers y pm.max_spare_servers, nunca por encima de pm.max_childrenla mayoría de los VPS
ondemandlos procesos arrancan solo cuando llega una petición y terminan tras pm.process_idle_timeout (10 s por defecto)muchos pools pequeños en un mismo servidor

En el modo dynamic, el número de procesos iniciales pm.start_servers es por defecto la media entre min_spare_servers y max_spare_servers.

El techo de pm.max_children lo marca la memoria, no la ambición. Un cálculo de ejemplo: el servidor tiene 8 GB de RAM y MySQL, Redis y el sistema ocupan 3 GB. Si un proceso de PHP-FPM con WordPress y WooCommerce ocupa en el pico unos 100 MB (mídelo en tu propio servidor, por ejemplo con la columna RSS de ps), quedan 5 GB para PHP, es decir, unos 50 procesos. Fijar 200 workers en esta situación no aumenta la capacidad; solo empuja al servidor al swap cuando llega la ola de tráfico.

Dos directivas ayudan con las fugas de memoria y los scripts colgados:

  • pm.max_requests reinicia un proceso tras un número fijo de peticiones. La documentación de PHP lo presenta como una forma de sortear fugas de memoria en bibliotecas de terceros. El valor por defecto es 0, es decir, sin límite.
  • request_terminate_timeout mata un proceso que dedica a una sola petición más tiempo del fijado, cuando max_execution_time por algún motivo no detiene el script. El valor por defecto es 0, es decir, desactivado.

#Cómo saber si PHP-FPM tiene pocos workers

En lugar de adivinar, activa la página de estado del pool con la directiva pm.status_path (por ejemplo /fpm-status) y exponla en Nginx solo para las direcciones de administración. Los campos que describe la documentación de la página de estado de FPM responden directamente a la pregunta sobre los workers:

  • listen queue: cuántas peticiones esperan ahora mismo un proceso libre.
  • max listen queue: la cola más larga desde el arranque del pool.
  • active processes e idle processes: cuántos procesos trabajan y cuántos esperan.
  • max children reached: un valor de 1 o más significa que el pool alcanzó pm.max_children al menos una vez.
  • slow requests: número de peticiones que superaron request_slowlog_timeout.

Todos los contadores vuelven a cero al reiniciar FPM. Si max children reached crece y la cola no está vacía, faltan workers o las páginas se generan demasiado despacio. Para saber cuál de las dos cosas pasa, define request_slowlog_timeout (por ejemplo 5 s) y una ruta para slowlog. PHP escribirá entonces un backtrace de cada petición lenta, es decir, el nombre del plugin y de la función que retiene al worker.


#Cómo la caché de página completa evita PHP con mucho tráfico

El PHP más rápido es el que no se ejecuta.

Si tu página es estática (por ejemplo, un artículo de blog), no tiene sentido que PHP la genere de cero cada vez. Deberías servir un archivo HTML ya terminado.

#Tipos de caché de página completa en WordPress

  1. Plugins (básicos): WP Rocket, W3 Total Cache. Guardan archivos HTML en disco. Bastan para sitios pequeños, pero cuando la petición pasa igualmente por PHP, el worker queda ocupado a pesar de la caché.
  2. Caché en el servidor (profesional):
    • Varnish: un servidor proxy que guarda las páginas terminadas en RAM delante de Apache o Nginx.
    • Nginx FastCGI Cache: integrada en Nginx. Muy eficiente.
    • Cloudflare (caché en el borde): sirve la página desde los servidores de Cloudflare antes de que el tráfico llegue siquiera a tu alojamiento.

Regla: el tráfico anónimo (sin sesión iniciada) no debería tocar PHP en absoluto. Cada página vista servida desde la caché es un worker libre para el carrito, el panel y los usuarios con sesión iniciada.


#Cómo configurar Varnish para WordPress y las cookies

Tras la instalación, Varnish a menudo no guarda casi nada en caché en WordPress, y eso concuerda con la documentación. Según la guía de Varnish para aumentar la tasa de aciertos, la configuración predeterminada no almacena respuestas con la cabecera Set-Cookie, y cuando el cliente envía una cabecera Cookie, Varnish omite la caché y va directo al backend. En un sitio con Google Analytics o con un banner de consentimiento, casi todos los visitantes tienen alguna cookie.

La solución de la documentación es eliminar cookies en vcl_recv en todos los casos en que el backend no las necesita. WordPress reconoce al usuario con sesión iniciada por las cookies con prefijos definidos en el núcleo (wordpress_, wordpress_sec_, wordpress_logged_in_), y a quien comenta por comment_author_. Esas tienen que llegar a PHP; el resto se puede eliminar:

sub vcl_recv {
    if (req.url ~ "^/(wp-admin|wp-login\.php|carrito|finalizar-compra|mi-cuenta)") {
        return (pass);
    }
    if (req.http.Cookie !~ "(wordpress_|wp-postpass_|comment_author_|woocommerce_items_in_cart|wp_woocommerce_session_)") {
        unset req.http.Cookie;
    }
}

Las rutas del carrito y del pago del ejemplo son ilustrativas; ajústalas a los slugs de tu tienda. Las versiones más recientes de Varnish dividen el vcl_recv integrado en subrutinas más pequeñas, y la descripción del VCL integrado muestra que la regla de las cookies se puede desactivar por separado mediante vcl_req_cookie. Solo tiene sentido cuando el backend indica siempre por sí mismo qué se puede guardar en caché.

#Cómo purgar la caché de Varnish tras publicar una entrada

Un tiempo de vida largo de los objetos eleva la tasa de aciertos, pero después de editar una entrada el lector tiene que ver la versión nueva. La documentación de Varnish sobre purge y ban describe dos mecanismos:

acl purge {
    "localhost";
    "192.168.55.0"/24;
}

sub vcl_recv {
    if (req.method == "PURGE") {
        if (!client.ip ~ purge) {
            return (synth(405, "Not allowed."));
        }
        return (purge);
    }
}
  • Purge elimina un objeto junto con todas las variantes que resultan de la cabecera Vary. El plugin que limpia la caché envía una petición PURGE a la dirección de la entrada. La lista acl es obligatoria, porque sin ella cualquiera en internet puede vaciar tu caché.
  • Ban funciona como un filtro sobre objetos ya guardados en la caché, por ejemplo “todo lo que hay bajo /categoria/”. Los bans que solo comprueban obj.* los procesa en segundo plano el hilo “ban lurker”. La documentación recomienda evitar req.* en los bans, porque el objeto de la petición no está disponible en ese hilo.

#Cómo la caché de objetos Redis alivia la base de datos de WordPress

¿Y las tiendas WooCommerce o los paneles de cliente que no se pueden guardar del todo en caché como HTML? Ahí PHP tiene que trabajar.

En cada carga, WordPress ejecuta decenas de las mismas consultas SQL: “obtén las opciones del sitio”, “obtén los ID de las categorías”, “obtén los metadatos del usuario”.

Redis (caché de objetos) guarda los resultados de esas consultas en RAM. En lugar de preguntar a MySQL, PHP pregunta a Redis.

Cuántas consultas desaparecen depende del tema y de los plugins. Puedes comprobarlo tú mismo: Query Monitor muestra el número de consultas SQL de una página antes y después de activar Redis.

En 2026, ejecutar WooCommerce sin Redis es buscarse problemas.


#Qué cambia el archivo object-cache.php en WordPress

WordPress tiene su propia caché de objetos, solo que no persistente. La documentación de la clase WP_Object_Cache dice claramente que, por defecto, la caché guarda los datos solo en memoria y solo hasta el final de la petición actual. Cada página vista empieza con la caché vacía y vuelve a consultar la base de datos.

La persistencia la aporta un drop-in. La función wp_start_object_cache() carga el archivo wp-content/object-cache.php si existe, y a partir de ese momento wp_cache_get(), wp_cache_set() y wp_cache_delete() hablan con Redis en lugar de con un array en la memoria del proceso. Plugins como Redis Object Cache hacen en la práctica una sola cosa: copian ese archivo y le pasan los datos de conexión.

Tres consecuencias fáciles de pasar por alto:

  • Los transitorios salen de la base de datos. Cuando hay una caché de objetos externa, set_transient() y delete_transient() guardan los datos a través de wp_cache_* y no en la tabla wp_options (set_transient). Vaciar Redis borra, por tanto, también todos los transitorios.
  • No todo va a Redis. El núcleo marca los grupos counts, plugins y theme_json como no persistentes mediante wp_cache_add_non_persistent_groups(), así que siguen viviendo solo en la memoria de una petición.
  • Cuándo el propio WordPress sugiere Redis. Desde la versión 6.1, Salud del sitio recomienda una caché de objetos persistente en toda instalación multisitio, y también cuando las opciones de carga automática superan las 500 entradas o los 100.000 bytes, o cuando alguna de las tablas de comentarios, opciones, entradas, términos o usuarios tiene al menos 1000 filas. Los umbrales se pueden cambiar con el filtro site_status_persistent_object_cache_thresholds.

#Cómo configurar maxmemory y la política de expulsión en Redis

Redis sin límite de memoria crece hasta que se acaba la RAM. En sistemas de 64 bits, maxmemory 0, es decir, sin límite, es el valor por defecto (redis.io, eviction). En un servidor que comparte memoria con PHP-FPM y MySQL, fija el límite de forma explícita:

maxmemory 512mb
maxmemory-policy allkeys-lru

Los 512 MB son un ejemplo, no una recomendación; elige el tamaño según el volumen de datos. Más importante es la política. La documentación de Redis llama a allkeys-lru una buena opción por defecto cuando una parte de las claves se lee mucho más que el resto, que es el retrato típico de una caché de WordPress. Las políticas volatile-* solo expulsan claves con tiempo de expiración y, cuando no hay claves así, se comportan como noeviction, es decir, Redis empieza a devolver errores al escribir. En WordPress, wp_cache_set() tiene por defecto $expire = 0, así que buena parte de las claves puede no tener TTL. Una política volatile-lru en una caché así acaba en errores de escritura en lugar de sustituir datos antiguos.

Para comprobar si la caché funciona, usa INFO stats: la tasa de aciertos es keyspace_hits / (keyspace_hits + keyspace_misses). Un evicted_keys alto con una tasa de aciertos baja significa que el límite de memoria es demasiado pequeño o que la política expulsa las claves equivocadas.


#Cómo excluir el carrito de WooCommerce de la caché

La documentación de WooCommerce sobre la configuración de plugins de caché indica tres páginas que deben seguir siendo dinámicas porque muestran datos de un cliente concreto: carrito, finalizar compra y mi cuenta. Indica también las cookies ante las que la caché tiene que ceder: woocommerce_cart_hash y woocommerce_items_in_cart siguen los cambios en el carrito, y wp_woocommerce_session_ identifica el carrito del cliente. Si una caché de base de datos en el servidor abarca las sesiones, hay que excluir _wc_session_.

La misma documentación advierte que algunos alojamientos con caché en el servidor guardan en caché la página mi cuenta, lo que acaba en un bucle al restablecer la contraseña. Es el primer síntoma que hay que revisar cuando, tras activar Varnish, los clientes dicen que “no se puede cambiar la contraseña”.


#Cómo configurar la caché FastCGI de Nginx en lugar de Varnish

Si el servidor ya ejecuta Nginx, no hace falta un Varnish aparte. El módulo ngx_http_fastcgi_module guarda en caché las respuestas de PHP-FPM en disco y mantiene las claves en memoria compartida. La documentación de Nginx indica que una zona de 1 MB almacena unas 8 mil claves.

fastcgi_cache_path /var/cache/nginx/wp levels=1:2 keys_zone=wp:10m inactive=60m;

server {
    set $skip_cache 0;
    if ($http_cookie ~* "wordpress_logged_in_|wp-postpass_|comment_author_|woocommerce_items_in_cart|wp_woocommerce_session_") {
        set $skip_cache 1;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php-fpm.sock;
        include fastcgi.conf;
        fastcgi_cache wp;
        fastcgi_cache_key "$scheme$request_method$host$request_uri";
        fastcgi_cache_valid 200 301 302 10m;
        fastcgi_cache_bypass $skip_cache;
        fastcgi_no_cache $skip_cache;
        fastcgi_cache_lock on;
        fastcgi_cache_use_stale updating error timeout;
        fastcgi_cache_background_update on;
    }
}

Qué hacen las directivas clave, según la documentación de Nginx:

  • fastcgi_cache_bypass no toma la respuesta de la caché y fastcgi_no_cache no la guarda cuando alguno de los valores no está vacío y es distinto de “0”. Por eso en el ejemplo ambas reciben la misma variable.
  • fastcgi_cache_lock on deja pasar a PHP una sola petición para rellenar una entrada nueva de la caché. Las demás esperan. Esto es lo que protege a los workers cuando, tras publicarse un enlace, cientos de personas abren la misma dirección que aún no está en caché.
  • fastcgi_cache_use_stale updating junto con fastcgi_cache_background_update on entrega al lector una copia anterior y hace la actualización en segundo plano.
  • El parámetro inactive elimina las entradas que nadie ha leído durante el tiempo indicado, sea cual sea su frescura. Por defecto son 10 minutos.

#Por qué se cayó WordPress en el Black Friday

Esta es la historia de una de nuestras caídas, que analizamos en un encuentro WordUp.

Paciente: un portal de noticias. Síntomas: un pico repentino de tráfico (Black Friday), el servidor cayó en 3 minutos. CPU al 100 %.

#Causas de la caída

  1. El AJAX asesino: Un plugin “Contador de visitas” enviaba una petición AJAX (admin-ajax.php) en cada visita para contarla.

    • AJAX en WordPress siempre arranca todo el motor PHP (todos los plugins). El archivo admin-ajax.php carga wp-load.php y después también los archivos del panel de administración.
    • Esa petición NO estaba en caché.
    • 10.000 visitas = 10.000 arranques completos de WordPress. El límite de PHP workers se agotó en 10 segundos.
  2. Sin índices en la base de datos: La tabla de entradas tenía 500.000 registros. Un plugin de “Entradas relacionadas” hacía ORDER BY RAND(). Eso hundió la base de datos MySQL.

#Cómo se resolvió la caída

  1. AJAX desactivado: sustituimos el contador por Google Analytics (JavaScript en el navegador).
  2. Redis: la implantación de Redis redujo la carga de MySQL en un 90 %.
  3. Varnish: configuramos Varnish para servir la página de inicio desde la RAM.

Resultado en esta caída: el servidor que se ahogaba con 100 usuarios pasó a atender a 5000 usuarios a la vez y la CPU bajó al 5 %. Son cifras de un solo sitio, no una norma para cualquier instalación.


#Cómo monitorizar el rendimiento de WordPress

No adivines “por qué va lento”.

  1. Query Monitor (desarrollo): un plugin que muestra las consultas SQL lentas y el uso de memoria en la página actual.
  2. New Relic (profesional): monitorización APM avanzada. Muestra exactamente qué función PHP o qué plugin consume más tiempo.
  3. Estado de PHP-FPM: pm.status_path y el slowlog, descritos más arriba.
  4. Redis: INFO stats e INFO memory.
  5. Logs: tail -f /var/log/nginx/error.log (consulta nuestra guía de SSH).

#Lista de comprobación de WordPress para mucho tráfico

¿Te preparas para un pico de tráfico?

  1. Alojamiento: elige uno con Redis y un límite de PHP workers configurable (por ejemplo, un servidor dedicado o un VPS en la nube).
  2. Workers: calcula pm.max_children a partir de la RAM y activa pm.status_path.
  3. Caché: configura Nginx FastCGI Cache o Varnish, con una lista de cookies que omiten la caché y una ACL para PURGE.
  4. Base de datos: instala un plugin de caché de objetos (por ejemplo, Redis Object Cache) y fija maxmemory y allkeys-lru.
  5. Tienda: excluye de la caché el carrito, finalizar compra y mi cuenta.
  6. Limpieza: desactiva los plugins que usan admin-ajax.php en el frontend.
  7. CDN: lleva las imágenes y el CSS a Cloudflare.

Un WordPress estable no es magia. Es aritmética. Reduce el tiempo de generación (caché) y aumenta la capacidad (workers y Redis).

¿Necesitas ayuda para escalar? Nuestro servicio de especialista WordPress prepara sitios para mucho tráfico. Consulta los precios de WordPress.

Siguiente paso

Transforma el artículo en una implementación real

Este bloque refuerza el enlazado interno y lleva al lector al siguiente paso más útil dentro de la arquitectura del sitio.

¿Quieres implementar esto en tu sitio?

Si el problema está en los Core Web Vitals, en el rendering lento o en el peso de WordPress, puedo mapear e implementar la optimización.

Preguntas frecuentes

Respuestas prácticas para aplicar el tema en la ejecución real.

¿Qué es la escalabilidad de WordPress?#
Consiste en mantener WordPress rápido y estable bajo alto tráfico mediante caché (Varnish/Redis), una configuración adecuada del servidor y control sobre los PHP workers.

¿Necesitas un FAQ adaptado a tu sector y mercado? Preparamos una versión alineada con tus objetivos de negocio.

Hablemos

Artículos Relacionados