Un WordPress recién instalado genera cada página desde cero: PHP compila el código, lanza decenas de consultas a MySQL y monta el HTML en cada visita. Con unos pocos ajustes en el servidor ese trabajo se reduce drásticamente. En este tutorial optimizarás un WordPress que corre en Ubuntu 24.04 con Nginx, PHP 8.3-FPM y MySQL: activarás OPcache, dimensionarás PHP-FPM y MySQL, añadirás Redis como caché de objetos y una caché de página FastCGI en Nginx, y medirás la mejora.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath. Los valores de ejemplo están pensados para 4 GB de RAM; ajústalos a tu servidor como se explica en cada paso.
  • Un usuario no root con privilegios sudo.
  • WordPress funcionando en /var/www/wordpress con Nginx, PHP 8.3-FPM y MySQL 8.0 (o MariaDB), con HTTPS. El bloque de servidor de los ejemplos está en /etc/nginx/sites-available/wordpress y el dominio es example.com: sustitúyelos por los tuyos.
  • WP-CLI instalado en /usr/local/bin/wp.

Paso 1: Medir el punto de partida

Sin una medida inicial no sabrás qué ajuste ha servido. El dato más útil en el servidor es el TTFB (tiempo hasta el primer byte), que refleja lo que tarda PHP en generar la página. Mídelo varias veces:

for i in 1 2 3; do curl -o /dev/null -s -w 'TTFB: %{time_starttransfer}s  Total: %{time_total}s\n' https://example.com/; done
TTFB: 0.842s  Total: 0.861s
TTFB: 0.795s  Total: 0.812s
TTFB: 0.810s  Total: 0.829s

Para ver cómo se comporta con carga concurrente, instala ab (Apache Bench) y lanza 200 peticiones con 10 clientes simultáneos:

sudo apt install -y apache2-utils
ab -n 200 -c 10 https://example.com/

Apunta el valor de Requests per second y Time per request. Repetirás estas mismas pruebas al final.

Paso 2: Activar y ajustar OPcache

OPcache guarda en memoria el código PHP ya compilado, de modo que no se recompila en cada petición. Viene incluido en PHP 8.3, pero los valores por defecto se quedan cortos para WordPress con muchos plugins. Crea un archivo de configuración propio:

sudo nano /etc/php/8.3/fpm/conf.d/99-wordpress-opcache.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=1
opcache.revalidate_freq=60

revalidate_freq=60 hace que PHP compruebe cada 60 segundos si un archivo ha cambiado, en lugar de en cada petición. Tras actualizar un plugin, los cambios pueden tardar hasta un minuto en verse, o reinicia PHP-FPM para aplicarlos al momento.

Reinicia PHP-FPM y comprueba que los valores se han cargado:

sudo systemctl restart php8.3-fpm
php-fpm8.3 -i | grep -E 'opcache.enable =>|opcache.memory_consumption'
opcache.enable => On => On
opcache.memory_consumption => 256 => 256

Paso 3: Dimensionar PHP-FPM

PHP-FPM mantiene un grupo de procesos que atienden las peticiones. Si hay pocos, las peticiones hacen cola; si hay demasiados, el servidor se queda sin memoria y empieza a usar swap, que es mucho peor. El número correcto depende de cuánta memoria usa cada proceso en tu sitio. Mídelo con el sitio en uso:

ps --no-headers -o rss -C php-fpm8.3 | awk '{sum+=$1; n++} END {printf "%d procesos, media %.0f MB\n", n, sum/n/1024}'
6 procesos, media 72 MB

Calcula pm.max_children como la memoria que puedes dedicar a PHP dividida entre ese consumo medio. En un servidor de 4 GB, reservando unos 1,8 GB para el sistema, MySQL y Redis, quedan unos 2,2 GB para PHP: 2200 / 72 ≈ 30 procesos.

Edita el grupo de procesos por defecto:

sudo nano /etc/php/8.3/fpm/pool.d/www.conf

Busca y ajusta estas directivas (las de slowlog están comentadas; descoméntalas):

pm = dynamic
pm.max_children = 30
pm.start_servers = 6
pm.min_spare_servers = 4
pm.max_spare_servers = 10
pm.max_requests = 500

request_slowlog_timeout = 5s
slowlog = /var/log/php8.3-fpm-slow.log
  • pm.max_requests = 500 recicla cada proceso tras 500 peticiones, lo que evita que una fuga de memoria de un plugin crezca sin límite.
  • request_slowlog_timeout registra la traza de cualquier petición que tarde más de 5 segundos, con la función de PHP en la que estaba. Es la forma más rápida de encontrar el plugin que ralentiza el sitio.

Aumenta también el límite de memoria de PHP para WordPress:

sudo nano /etc/php/8.3/fpm/conf.d/99-wordpress.ini
memory_limit = 256M

Comprueba la configuración y reinicia:

sudo php-fpm8.3 -t
sudo systemctl restart php8.3-fpm
NOTICE: configuration file /etc/php/8.3/fpm/php-fpm.conf test is successful

Si más adelante ves en /var/log/php8.3-fpm.log el aviso server reached pm.max_children setting, primero comprueba si hay memoria libre (free -h) antes de subir el valor.

Paso 4: Ajustar MySQL y revisar la base de datos

El ajuste que más influye en MySQL es innodb_buffer_pool_size: la memoria donde InnoDB guarda las tablas e índices. Si la base de datos cabe entera en ella, las lecturas no tocan el disco. Consulta primero el tamaño de tu base de datos (sustituye wordpress por su nombre):

sudo mysql -e "SELECT table_schema, ROUND(SUM(data_length + index_length)/1024/1024) AS size_mb FROM information_schema.tables WHERE table_schema = 'wordpress' GROUP BY table_schema;"
+--------------+---------+
| table_schema | size_mb |
+--------------+---------+
| wordpress    |     420 |
+--------------+---------+

Crea un archivo de configuración propio. En MySQL va en /etc/mysql/mysql.conf.d/; en MariaDB, en /etc/mysql/mariadb.conf.d/:

sudo nano /etc/mysql/mysql.conf.d/99-wordpress.cnf
[mysqld]
innodb_buffer_pool_size = 1G
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1

Con una base de datos de 420 MB, 1 GB deja margen para crecer. Nunca le des más de la mitad de la RAM en un servidor que también ejecuta PHP. El registro de consultas lentas guarda las que tarden más de un segundo. Reinicia MySQL:

sudo systemctl restart mysql
sudo mysql -e "SELECT @@innodb_buffer_pool_size/1024/1024 AS buffer_pool_mb;"
+----------------+
| buffer_pool_mb |
+----------------+
|  1024.00000000 |
+----------------+

Tras unos días de tráfico, resume las consultas lentas agrupadas por patrón:

sudo mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

Revisar las opciones autocargadas

WordPress carga en cada petición todas las filas de wp_options marcadas como autocargadas. Plugins desinstalados suelen dejar ahí datos que ya nadie usa. Mide el total (WordPress 6.6 cambió los valores de la columna autoload, por eso la consulta incluye los antiguos y los nuevos):

sudo -u www-data wp --path=/var/www/wordpress db query "SELECT ROUND(SUM(LENGTH(option_value))/1024) AS autoload_kb FROM wp_options WHERE autoload IN ('yes','on','auto','auto-on');"

Si supera 1 MB, lista las opciones más grandes para ver qué plugin las creó:

sudo -u www-data wp --path=/var/www/wordpress db query "SELECT option_name, ROUND(LENGTH(option_value)/1024) AS kb FROM wp_options WHERE autoload IN ('yes','on','auto','auto-on') ORDER BY LENGTH(option_value) DESC LIMIT 10;"

Si una opción pertenece a un plugin que ya no usas, bórrala con wp option delete nombre_opcion, siempre después de hacer una copia de la base de datos. Elimina también los transitorios caducados:

sudo -u www-data wp --path=/var/www/wordpress transient delete --expired

Si tu prefijo de tablas no es wp_, cámbialo en las consultas.

Paso 5: Añadir Redis como caché de objetos

La caché de objetos guarda el resultado de las consultas a la base de datos entre peticiones. Sin ella, WordPress repite las mismas consultas en cada visita. Es especialmente útil para usuarios identificados, el panel de administración y tiendas WooCommerce, que no pueden usar la caché de página.

Instala Redis y la extensión de PHP:

sudo apt install -y redis-server php-redis

Limita la memoria de Redis y haz que descarte las claves menos usadas cuando se llene, en lugar de devolver errores:

sudo nano /etc/redis/redis.conf

Busca las directivas maxmemory y maxmemory-policy (están comentadas) y déjalas así:

maxmemory 256mb
maxmemory-policy allkeys-lru

Reinicia Redis y PHP-FPM, y comprueba que Redis responde:

sudo systemctl restart redis-server php8.3-fpm
redis-cli ping
PONG

Define un prefijo para las claves de este sitio (evita colisiones si varios WordPress comparten el mismo Redis) e instala el plugin Redis Object Cache:

sudo -u www-data wp --path=/var/www/wordpress config set WP_REDIS_PREFIX 'example_'
sudo -u www-data wp --path=/var/www/wordpress plugin install redis-cache --activate
sudo -u www-data wp --path=/var/www/wordpress redis enable

wp redis enable copia el archivo object-cache.php en wp-content, que es el que conecta WordPress con Redis. Comprueba el estado:

sudo -u www-data wp --path=/var/www/wordpress redis status
Status: Connected
Client: PhpRedis (v5.3.7)
Drop-in: Valid

Navega unos minutos por el sitio y comprueba que Redis acierta en la mayoría de las lecturas:

redis-cli info stats | grep -E 'keyspace_(hits|misses)'
keyspace_hits:18452
keyspace_misses:1210

Paso 6: Configurar la caché de página FastCGI en Nginx

La caché de página es el cambio con más impacto: Nginx guarda el HTML generado y lo sirve directamente a los visitantes anónimos sin llamar a PHP ni a MySQL. Hay que excluir con cuidado todo lo que sea personal: usuarios identificados, el panel, formularios enviados por POST y, si usas WooCommerce, carrito y checkout.

Crea el directorio de caché:

sudo install -d -o www-data -g www-data /var/cache/nginx/wordpress

La zona de caché y las reglas de exclusión van en el contexto http. Crea un archivo en conf.d, que Nginx incluye automáticamente:

sudo nano /etc/nginx/conf.d/wordpress-cache.conf
fastcgi_cache_path /var/cache/nginx/wordpress levels=1:2 keys_zone=WORDPRESS:100m max_size=1g inactive=60m use_temp_path=off;
fastcgi_cache_key "$scheme$request_method$host$request_uri";

# 1 = no usar la caché para esta petición
map $request_method $skip_cache_method {
    default 1;
    GET     0;
    HEAD    0;
}

map $query_string $skip_cache_args {
    default 1;
    ""      0;
}

map $request_uri $skip_cache_uri {
    default                         0;
    ~*^/wp-admin/                   1;
    ~*^/wp-(login|cron|signup)\.php 1;
    ~*^/xmlrpc\.php                 1;
    ~*^/wp-json/                    1;
    ~*^/(cart|checkout|my-account)/ 1;
}

map $http_cookie $skip_cache_cookie {
    default                   0;
    ~*wordpress_logged_in_    1;
    ~*wp-postpass_            1;
    ~*comment_author_         1;
    ~*woocommerce_items_in_cart 1;
}

Si tu tienda usa otros slugs para el carrito o la cuenta (por ejemplo /carrito/ y /finalizar-compra/), añádelos a $skip_cache_uri.

Ahora activa la caché en el bloque location de PHP de tu sitio:

sudo nano /etc/nginx/sites-available/wordpress

Sustituye el bloque location ~ \.php$ por este:

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;

    fastcgi_cache WORDPRESS;
    fastcgi_cache_valid 200 301 60m;
    fastcgi_cache_valid 404 1m;
    fastcgi_cache_bypass $skip_cache_method $skip_cache_args $skip_cache_uri $skip_cache_cookie;
    fastcgi_no_cache $skip_cache_method $skip_cache_args $skip_cache_uri $skip_cache_cookie;
    fastcgi_cache_use_stale error timeout updating http_500 http_503;
    fastcgi_cache_lock on;
    add_header X-Cache-Status $upstream_cache_status always;
}
  • fastcgi_cache_bypass y fastcgi_no_cache se saltan la caché si cualquiera de las variables vale 1.
  • Nginx no guarda por defecto las respuestas que incluyen Set-Cookie, lo que añade una protección extra frente a cachear contenido personal.
  • fastcgi_cache_use_stale sirve la copia anterior si PHP falla o mientras se regenera.

Comprueba la sintaxis, recarga y pide la portada dos veces:

sudo nginx -t && sudo systemctl reload nginx
curl -s -o /dev/null -D - https://example.com/ | grep -i x-cache-status
curl -s -o /dev/null -D - https://example.com/ | grep -i x-cache-status
x-cache-status: MISS
x-cache-status: HIT

Comprueba también que el panel nunca se cachea:

curl -s -o /dev/null -D - https://example.com/wp-login.php | grep -i x-cache-status
x-cache-status: BYPASS

Purgar la caché al publicar

Sin purga, un artículo editado seguiría mostrándose en su versión antigua hasta 60 minutos. El plugin Nginx Helper borra de la caché las páginas afectadas cada vez que publicas o editas contenido. Indícale dónde está la caché e instálalo:

sudo -u www-data wp --path=/var/www/wordpress config set RT_WP_NGINX_HELPER_CACHE_PATH '/var/cache/nginx/wordpress/'
sudo -u www-data wp --path=/var/www/wordpress plugin install nginx-helper --activate

En Ajustes > Nginx Helper, marca Enable Purge, elige nginx Fastcgi cache y como método de purga Delete local server cache files. Este método calcula el nombre del archivo a partir de la misma clave $scheme$request_method$host$request_uri que has configurado, por eso no debes cambiar fastcgi_cache_key.

Para vaciar toda la caché a mano, por ejemplo tras cambiar de tema:

sudo find /var/cache/nginx/wordpress -type f -delete

Paso 7: Comprimir y cachear los archivos estáticos

Ubuntu activa gzip en Nginx pero solo para HTML. Amplíalo a CSS, JavaScript, JSON y SVG:

sudo nano /etc/nginx/conf.d/gzip.conf
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types text/plain text/css text/xml application/javascript application/json application/xml application/rss+xml image/svg+xml;

Las imágenes y fuentes ya están comprimidas y no se incluyen. Para que el navegador guarde los estáticos y no los vuelva a pedir en cada visita, añade este bloque dentro del server de tu sitio, junto a los demás location:

sudo nano /etc/nginx/sites-available/wordpress
location ~* \.(?:css|js|mjs|jpg|jpeg|png|gif|webp|avif|svg|ico|woff2?)$ {
    expires 30d;
    access_log off;
    try_files $uri =404;
}

WordPress añade ?ver= a las URL de CSS y JS de temas y plugins, así que al actualizarlos los navegadores descargan la versión nueva aunque la anterior siga en caché. Recarga y comprueba las cabeceras de un archivo CSS de tu tema:

sudo nginx -t && sudo systemctl reload nginx
curl -s -o /dev/null -D - -H 'Accept-Encoding: gzip' https://example.com/wp-includes/css/dist/block-library/style.min.css | grep -iE 'content-encoding|cache-control'
content-encoding: gzip
cache-control: max-age=2592000

Paso 8: Mover WP-Cron al cron del sistema

Por defecto WordPress comprueba sus tareas programadas en cada visita, lo que añade trabajo a peticiones de usuarios reales y, con la caché de página, puede no ejecutarse nunca porque PHP apenas recibe visitas. Desactívalo:

sudo -u www-data wp --path=/var/www/wordpress config set DISABLE_WP_CRON true --raw

Y lánzalo cada cinco minutos desde el cron del sistema:

sudo nano /etc/cron.d/wordpress
*/5 * * * * www-data /usr/local/bin/wp --path=/var/www/wordpress cron event run --due-now --quiet

Comprueba que las tareas se ejecutan correctamente a mano:

sudo -u www-data wp --path=/var/www/wordpress cron event run --due-now
Success: Executed a total of 4 cron events.

Paso 9: Medir de nuevo

Repite las pruebas del paso 1:

for i in 1 2 3; do curl -o /dev/null -s -w 'TTFB: %{time_starttransfer}s  Total: %{time_total}s\n' https://example.com/; done
ab -n 200 -c 10 https://example.com/
TTFB: 0.021s  Total: 0.024s
TTFB: 0.018s  Total: 0.020s
TTFB: 0.019s  Total: 0.021s

Las páginas servidas desde la caché de Nginx bajan de cientos de milisegundos a unas decenas, y las peticiones por segundo de ab se multiplican. Para las páginas que no se cachean (panel, carrito, usuarios identificados) la mejora viene de OPcache, Redis y el ajuste de MySQL: compruébalo midiendo, por ejemplo, https://example.com/wp-login.php.

Solución de problemas

  • Los usuarios identificados ven contenido de otros o el carrito vacío: alguna ruta personal se está cacheando. Revisa X-Cache-Status de esa URL, que debe ser BYPASS, y añade su ruta o su cookie a los map del paso 6.
  • 502 Bad Gateway tras cambiar PHP-FPM: revisa sudo journalctl -u php8.3-fpm y /var/log/php8.3-fpm.log. Suele ser un error de sintaxis en www.conf; sudo php-fpm8.3 -t lo indica.
  • MySQL no arranca tras el cambio: innodb_buffer_pool_size es mayor que la memoria disponible. Bájalo y revisa sudo journalctl -u mysql.
  • wp redis status muestra Not connected: comprueba que redis-server está activo y que la extensión está cargada con php-fpm8.3 -m | grep redis.

Conclusión

Has medido tu WordPress, has activado OPcache, has dimensionado PHP-FPM y MySQL según tu memoria real, has añadido Redis como caché de objetos y una caché de página FastCGI con purga automática, y has comprobado la mejora con los mismos datos del principio. Como siguientes pasos, revisa con el slowlog de PHP-FPM y el de MySQL qué plugins siguen siendo lentos, sirve las imágenes en WebP o AVIF con un tamaño adecuado, y valora una CDN delante de Nginx si tu público está repartido por varios continentes.