WordPress al 100% de CPU: encuentra el plugin o bot que está saturando tu sitio

¿Te fue útil?

Cuando WordPress deja un núcleo al 100%, no se siente como “optimización de rendimiento”. Se siente como si tu sitio se estuviera derritiendo en público mientras intentas explicarle a alguien no técnico que, sí, el servidor está “arriba”, pero no, no está realmente funcionando.

La buena noticia: los picos de CPU suelen ser diagnosticables. La mala noticia: la gente tiende a adivinar. Desactivan plugins al azar, reinician PHP-FPM como si fuera un ritual y declaran victoria hasta el siguiente pico. Hagámoslo como gestionamos sistemas en producción: medir, atribuir, decidir y luego arreglar sin romper el proceso de compra.

Guion de diagnóstico rápido (haz esto primero)

Este es el camino de “detener la hemorragia e identificar al atacante”. No optimices. No refactorices. No discutas con la gráfica. Sigue la cadena de custodia desde la CPU hasta el proceso, la petición y el código.

1) Confirma que es saturación de CPU real (no solo “load”)

  • Revisa CPU, cola de ejecución y tiempo steal (las VMs mienten cuando los vecinos están ruidosos).
  • Decide: si el tiempo de steal es alto, tu app puede ser inocente; tu hipervisor no lo es.

2) Identifica qué familia de procesos está consumiendo CPU

  • ¿Son trabajadores php-fpm? ¿Un proceso fuera de control? ¿mysqld? ¿Algo más?
  • Decide: si PHP está caliente, el siguiente paso es la atribución por petición. Si MySQL está caliente, salta a consultas lentas.

3) Atribuye la CPU a una ruta de petición y cliente

  • Correlaciona el slowlog de PHP-FPM, los logs de acceso de Nginx/Apache y las IPs/URLs principales.
  • Decide: si una URL o una IP domina, mitiga en el borde ahora (WAF/límite de tasa) mientras depuras más a fondo.

4) Encuentra el punto de entrada de WordPress

  • Culpables comunes: wp-login.php, xmlrpc.php, /wp-json/, admin-ajax.php, endpoints de WooCommerce, búsquedas, generación de sitemaps.
  • Decide: si son endpoints de autenticación, trátalos como tráfico hostil. Si es admin-ajax, trátalo como comportamiento de plugin/tema hasta que se demuestre que es un bot.

5) Identifica el plugin/tema o la consulta

  • Usa las trazas del slowlog de PHP-FPM y el log de consultas lentas de MySQL.
  • Decide: desactiva/reemplaza el plugin, parchea la configuración o cachea agresivamente—basado en evidencia, no en intuiciones.

6) Aplica un límite temporal seguro

  • Limita la tasa de rutas abusivas, activa cache y establece límites sensatos en PHP-FPM.
  • Decide: elige degradación controlada (429 en endpoints abusivos) sobre la muerte total del sitio.

Qué significa realmente “100% de CPU” en WordPress

En el ecosistema WordPress, “CPU al 100%” suele significar que PHP está haciendo demasiado trabajo con demasiada frecuencia. También puede significar que muchos workers de PHP están ejecutables al mismo tiempo porque un plugin disparó consultas costosas o llamadas remotas. O puede significar que MySQL está en llamas y PHP solo está esperando, pero tus gráficas son demasiado toscas para distinguirlo.

Algunos chequeos de realidad:

  • Un núcleo saturado en una máquina multi-núcleo puede seguir siendo catastrófico si PHP-FPM atiende solicitudes en un solo hilo por petición y tu endpoint más caliente serializa trabajo (locks, sesiones, avalanchas de cache).
  • Load average no es CPU. El load puede ser iowait, cola de ejecuciones o procesos bloqueados. Revisa iowait y la cola de ejecución.
  • “Pero tenemos cache” no significa que estés a salvo. Usuarios autenticados, carrito/checkout, admin-ajax y páginas personalizadas eluden la mayoría de caches por diseño.

Idea parafraseada de Werner Vogels (CTO de Amazon): Todo falla eventualmente; diseña para detectar, limitar el radio de impacto y recuperarte rápido.

Y sí, a veces la causa es hilarantemente mundana: una tarea cron que se ejecuta cada minuto porque alguien copió un fragmento de “mejora” de un foro.

Broma corta #1: WordPress no “golpea” el 100% de CPU al azar. Simplemente está muy comprometido con el caos que permitiste.

Datos interesantes y contexto (historia breve y útil)

  1. WordPress se lanzó en 2003 como un fork de b2/cafelog; heredó el modelo “PHP renderiza todo por petición” que hace que el coste por petición importe.
  2. wp-cron no es un cron real por defecto. Se dispara con el tráfico del sitio, lo que significa que “más visitantes” puede accidentalmente significar “más ejecuciones de cron”.
  3. xmlrpc.php fue un triunfo de compatibilidad (publicación remota, clientes móviles), y luego se convirtió en un objetivo favorito para credential stuffing y amplificación de pingback.
  4. admin-ajax.php se volvió una navaja suiza para plugins porque es cómodo; también es un generador de carga involuntario cuando se usa para sondeos frecuentes desde el front-end.
  5. PHP-FPM reemplazó a mod_php como patrón común de despliegue porque aisló pools y mejoró la estabilidad, pero también facilitó errores de “max_children” a escala.
  6. El caché de objetos cambió los cuellos de botella: añadir Redis/Memcached puede reducir la carga de MySQL, pero también puede ocultar código malo hasta que los misses o las expulsiones causen avalanchas.
  7. WooCommerce cambió la forma del tráfico: carritos, sesiones, fragmentos AJAX y comportamiento de usuarios autenticados invalidan el cache de página más que un blog.
  8. HTTP/2 redujo la sobrecarga de conexión, pero puede aumentar la concurrencia de solicitudes, lo que hace que los endpoints costosos fallen más rápido si no límites la tasa.
  9. “Headless” WP aumentó el uso de API: el tráfico intenso a /wp-json/ puede comportarse como un DDoS de baja intensidad si las consultas no están acotadas o cacheadas.

Tareas prácticas: comandos, salidas, decisiones (el kit básico)

Quieres tareas repetibles que te lleven de “CPU mala” a “este plugin y este endpoint, desde estas IPs, a esta tasa”. Abajo están las tareas que puedes ejecutar en un VPS o VM Linux típico con Nginx/Apache + PHP-FPM + MySQL/MariaDB. Cada una incluye: comando, salida de ejemplo, qué significa y la decisión a tomar.

Tarea 1: Confirma saturación de CPU vs tiempo steal

cr0x@server:~$ mpstat -P ALL 1 5
Linux 6.5.0 (wp-prod-01)  12/27/2025  _x86_64_  (4 CPU)

12:01:11 PM  CPU   %usr %nice  %sys %iowait  %irq %soft  %steal %idle
12:01:12 PM  all  92.10  0.00  6.40   0.20   0.00  0.80   0.00  0.50
12:01:12 PM    0  99.00  0.00  1.00   0.00   0.00  0.00   0.00  0.00
12:01:12 PM    1  88.00  0.00 11.00   1.00   0.00  0.00   0.00  0.00
12:01:12 PM    2  92.00  0.00  7.00   1.00   0.00  0.00   0.00  0.00
12:01:12 PM    3  89.00  0.00 10.00   1.00   0.00  0.00   0.00  0.00

Significado: Alto %usr con %steal cercano a cero significa que tu carga está realmente consumiendo CPU. Bajo %idle confirma saturación.

Decisión: Sigue investigando en la aplicación. Si %steal fuera alto (>5–10%), escalarías a la capa de hosting o migrarías.

Tarea 2: Encuentra qué procesos consumen CPU ahora

cr0x@server:~$ ps -eo pid,ppid,cmd,%cpu,%mem --sort=-%cpu | head -n 12
  PID  PPID CMD                         %CPU %MEM
19421 19310 php-fpm: pool www           88.4  2.1
19455 19310 php-fpm: pool www           72.9  2.0
19470 19310 php-fpm: pool www           65.1  2.1
19310     1 php-fpm: master process      2.1  0.4
 2214     1 nginx: worker process        1.3  0.3
 1870     1 mysqld                       0.9 10.5

Significado: Los workers de PHP-FPM son los que consumen, no Nginx. MySQL no es el punto caliente principal (por ahora).

Decisión: Cambia a la atribución por petición en PHP: slowlog, logs de acceso y tiempos upstream.

Tarea 3: Revisa la saturación del pool de PHP-FPM (¿hay cola?)

cr0x@server:~$ sudo ss -s
Total: 817 (kernel 0)
TCP:   613 (estab 402, closed 162, orphaned 0, synrecv 0, timewait 162/0), ports 0

Transport Total     IP        IPv6
RAW       0         0         0
UDP       6         4         2
TCP       451       380       71
INET      457       384       73
FRAG      0         0         0

Significado: Muchas sesiones TCP establecidas pueden significar tráfico normal… o respuestas lentas acumulándose.

Decisión: Revisa tiempos upstream de Nginx y el estado de PHP-FPM a continuación.

Tarea 4: Habilita y lee el estado de PHP-FPM (si está disponible)

cr0x@server:~$ curl -s http://127.0.0.1/fpm-status | head
pool:                 www
process manager:      dynamic
start time:           27/Dec/2025:11:02:13 +0000
start since:          3540
accepted conn:        918245
listen queue:         37
max listen queue:     221
listen queue len:     128

Significado: Una listen queue distinta de cero significa que las peticiones esperan un worker libre. Eso se traduce en latencia visible para el usuario.

Decisión: Puedes aumentar temporalmente pm.max_children si la RAM lo permite, pero primero averigua por qué los workers son lentos (código costoso, llamadas remotas, BD). Escalar trabajo malo solo genera más trabajo malo.

Tarea 5: Revisa las URLs principales en Nginx por tasa de peticiones

cr0x@server:~$ sudo awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head
  9412 /wp-admin/admin-ajax.php
  3120 /wp-login.php
  1788 /wp-json/wp/v2/posts?per_page=100
   904 /?s=shoes
   611 /xmlrpc.php

Significado: admin-ajax.php domina. Eso rara vez es “navegación normal”. Normalmente es una función de plugin, un script de tema o un bot que está sondeando endpoints.

Decisión: Identifica qué action= está caliente y luego mapea eso a un plugin.

Tarea 6: Desglosa admin-ajax por el parámetro action

cr0x@server:~$ sudo grep "admin-ajax.php" /var/log/nginx/access.log | awk -F'action=' '{print $2}' | awk '{print $1}' | cut -d'&' -f1 | sort | uniq -c | sort -nr | head
  8122 wc_fragment_refresh
   901 elementor_ajax
   214 wpforms_submit
   143 heartbeat

Significado: wc_fragment_refresh es la llamada clásica de fragmentos de WooCommerce para refrescar el carrito. Puede ser legítima, pero también es conocida por ser muy ruidosa y hostil al cache.

Decisión: Si el tráfico son compradores reales, optimiza la ruta de WooCommerce y las reglas de cache. Si son bots, limita la tasa/deniega según el comportamiento.

Tarea 7: Identifica las IPs cliente que golpean el endpoint

cr0x@server:~$ sudo grep "admin-ajax.php" /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -nr | head
  3022 203.0.113.50
  1877 198.51.100.77
   944 192.0.2.19
   611 10.0.0.12

Significado: Una IP generando miles de peticiones es sospechosa a menos que sea tu propio monitoreo, una prueba de carga o un proxy conocido.

Decisión: Si no son tus proxies, bloquea o limita la tasa inmediatamente en el borde o firewall. Luego sigue diagnosticando la causa raíz del resto del tráfico.

Tarea 8: Confirma si las peticiones son lentas según el tiempo upstream de Nginx

cr0x@server:~$ sudo awk '{print $(NF-1),$7,$1}' /var/log/nginx/access.log | head -n 5
0.842 /wp-admin/admin-ajax.php 203.0.113.50
1.102 /wp-admin/admin-ajax.php 203.0.113.50
0.019 /wp-login.php 198.51.100.77
0.611 /wp-admin/admin-ajax.php 192.0.2.19
0.955 /wp-json/wp/v2/posts?per_page=100 198.51.100.77

Significado: Si configuraste un formato de log que incluya el tiempo de respuesta upstream, puedes ver si el backend es lento. Valores cercanos a 1s para admin-ajax a escala cocinarán la CPU.

Decisión: Backend lento significa que necesitas slowlog de PHP y profiling de WP, no solo bloquear.

Tarea 9: Habilita PHP-FPM slowlog y captura trazas de pila

cr0x@server:~$ sudo grep -nE "request_slowlog_timeout|slowlog" /etc/php/8.2/fpm/pool.d/www.conf
308:request_slowlog_timeout = 5s
309:slowlog = /var/log/php-fpm/www-slow.log

Significado: Las peticiones que tarden más de 5 segundos volcarán un backtrace en el archivo slowlog.

Decisión: Actívalo durante una ventana de incidente. Si no puedes permitirte la sobrecarga de logging, pon el umbral más alto (10–15s) y mantenlo temporal.

cr0x@server:~$ sudo tail -n 25 /var/log/php-fpm/www-slow.log
[27-Dec-2025 12:03:41]  [pool www] pid 19455
script_filename = /var/www/html/wp-admin/admin-ajax.php
[0x00007f2a1c8b2a40] mysqli_query() /var/www/html/wp-includes/wp-db.php:2345
[0x00007f2a1c8b28b0] _do_query() /var/www/html/wp-includes/wp-db.php:2263
[0x00007f2a1c8b27f0] query() /var/www/html/wp-includes/wp-db.php:3307
[0x00007f2a1c8b2460] get_results() /var/www/html/wp-includes/wp-db.php:3650
[0x00007f2a1c8b1f90] wc_get_products() /var/www/html/wp-content/plugins/woocommerce/includes/wc-product-functions.php:1201
[0x00007f2a1c8b1c00] my_custom_fragments() /var/www/html/wp-content/plugins/some-fragments/plugin.php:88

Significado: Ahora tienes un camino desde un endpoint caliente hasta un archivo y función de plugin específico. Eso es oro.

Decisión: Desactiva/reemplaza el plugin o ajusta sus configuraciones. Si es tu código personalizado, arréglalo. Si es comportamiento del core de WooCommerce, considera ajustes de fragment caching o reducir llamadas.

Tarea 10: Usa WP-CLI para listar plugins y su estado

cr0x@server:~$ cd /var/www/html && sudo -u www-data wp plugin list --status=active
+---------------------+----------+-----------+---------+
| name                | status   | update    | version |
+---------------------+----------+-----------+---------+
| woocommerce         | active   | available | 8.9.2   |
| elementor           | active   | none      | 3.24.0  |
| some-fragments      | active   | none      | 1.6.1   |
| redis-cache         | active   | none      | 2.5.3   |
+---------------------+----------+-----------+---------+

Significado: Confirma que el plugin sospechoso está activo e identifica el estado de actualización (a veces ejecutas una versión con problemas conocidos).

Decisión: Si hay una actualización y el changelog indica correcciones de rendimiento/seguridad, prográmala. Si necesitas alivio inmediato, desactiva al culpable fuera de hora pico o en un canario.

Tarea 11: Desactiva temporalmente un plugin sospechoso (prueba controlada)

cr0x@server:~$ cd /var/www/html && sudo -u www-data wp plugin deactivate some-fragments
Plugin 'some-fragments' deactivated.

Significado: Elimina la ruta de código de producción sin editar archivos directamente.

Decisión: Observa CPU y tasa de peticiones. Si la CPU baja y los errores no se disparan, encontraste al culpable. Si aparecen errores, revierte y prueba una mitigación más estrecha (límite de tasa, cache, toggles de función).

Tarea 12: Revisa MySQL por consultas lentas (¿es BD el verdadero cuello de botella?)

cr0x@server:~$ sudo mysql -e "SHOW FULL PROCESSLIST\G" | head -n 30
*************************** 1. row ***************************
     Id: 12891
   User: wpuser
   Host: 127.0.0.1:51862
     db: wordpress
Command: Query
   Time: 9
  State: Sending data
   Info: SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1=1 AND wp_posts.post_type IN ('product') AND (wp_posts.post_status = 'publish') ORDER BY wp_posts.post_date DESC LIMIT 0, 48

Significado: Consultas que tardan 9 segundos no son “aceptables”. “Sending data” suele implicar escaneos grandes, índices pobres o conjuntos de resultados enormes.

Decisión: Habilita el log de consultas lentas, inspecciona patrones de consulta y arregla índices o reduce el costo de la consulta (paginación, restricciones, cache). Considera limitar consultas de API caras.

Tarea 13: Activa el log de consultas lentas de MySQL (temporal) e inspecciona

cr0x@server:~$ sudo mysql -e "SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1;"
cr0x@server:~$ sudo tail -n 20 /var/log/mysql/slow.log
# Time: 2025-12-27T12:06:01.123456Z
# User@Host: wpuser[wpuser] @ localhost []
# Query_time: 2.941  Lock_time: 0.001 Rows_sent: 48  Rows_examined: 504812
SET timestamp=1766837161;
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1=1 AND wp_posts.post_type IN ('product') ORDER BY wp_posts.post_date DESC LIMIT 0, 48;

Significado: Rows_examined en cientos de miles para un pequeño resultado de página es el clásico “estás escaneando el océano para encontrar un pez”.

Decisión: Identifica qué página/API lo dispara y considera reescribir la consulta vía configuración del plugin, añadir índices (con cuidado) o cachear a nivel de objeto/página.

Tarea 14: Revisa I/O de disco (la CPU podría estar “ocupada esperando” en otra parte)

cr0x@server:~$ iostat -x 1 3
Device            r/s   w/s  rkB/s  wkB/s  await  %util
nvme0n1          2.1  18.2   44.0  512.0   1.20  22.5

Significado: Bajo await y %util moderado significa que el almacenamiento probablemente no es el limitador. Si await es alto (decenas/centenas ms) y %util está al máximo, tu “problema de CPU” es en realidad presión de I/O.

Decisión: Si el almacenamiento es el cuello de botella, arregla I/O (optimiza BD, muévete a disco más rápido, reduce logging, ajusta buffer pool) antes de tocar PHP.

Tarea 15: Captura un backtrace en vivo de un worker PHP con perf (cuando estás desesperado)

cr0x@server:~$ sudo perf top -p 19421
Samples: 2K of event 'cpu-clock', 4000 Hz, Event count (approx.): 510000000
Overhead  Shared Object        Symbol
  18.20%  php-fpm8.2           zend_execute_ex
  11.35%  php-fpm8.2           zif_preg_match
   8.74%  libpcre2-8.so.0.11.2 pcre2_match_8
   6.10%  php-fpm8.2           zim_spl_autoload_call

Significado: Regex intensivo (preg_match) dentro de PHP puede ser comportamiento de un plugin (filtrado, parseo de contenido, escaneo de seguridad) o un tema haciendo cosas “inteligentes” por petición.

Decisión: Combina con el slowlog para localizar qué plugin lo llama. Si un plugin de seguridad está escaneando contenido en cada petición, puede ser hora de buscar una alternativa más tranquila.

¿Es un plugin, un bot o la plataforma?

La mayoría de los incidentes de CPU caen en uno de tres grupos:

  • Tráfico hostil: credential stuffing en wp-login.php, abuso de XML-RPC, fuerza bruta en endpoints REST, scraping agresivo o “herramientas SEO” que se comportan como locusts.
  • Comportamiento de plugin/tema: sondeos con admin-ajax, consultas costosas de productos, procesamiento de imágenes, balizas de analítica, constructores de páginas que hacen render en servidor o generación dinámica de CSS.
  • Restricciones de la plataforma: pocos núcleos de CPU, poca RAM (swap thrash), tiempo de steal por vecino ruidoso, PHP-FPM mal configurado, MySQL con recursos insuficientes.

El movimiento clave es atribuir por ruta de petición y por cliente. Un problema de plugin aparece como muchas IPs distintas golpeando el mismo endpoint. Un problema de bot suele mostrar un conjunto pequeño de IPs o ASN, user agents extraños o tasas de petición altas con baja continuidad de sesión.

La prueba de división en dos minutos

Si el endpoint caliente es wp-login.php o xmlrpc.php, asume hostilidad hasta demostrar lo contrario. Si el endpoint caliente es admin-ajax.php con una acción consistente, asume plugin/tema hasta demostrar lo contrario. Si es /wp-json/ con per_page=100 o consultas no acotadas, asume “problema de diseño de API”.

Broma corta #2: Si tu “automatización de marketing” golpea admin-ajax.php 50 veces por segundo, no es automatización; es una pequeña denegación de servicio con presupuesto de código.

Tres micro-historias del mundo corporativo (cosas que realmente pasan)

Micro-historia 1: Incidente causado por una suposición equivocada

Una empresa mediana ejecutaba WordPress como la cara pública de una suite de producto. Su rotación de SRE lo trataba como “contenido algo estático”, así que pusieron un CDN por delante, activaron cache de página y siguieron con otras cosas. Todos dormían mejor. Entonces llegó un martes por la mañana con un olor familiar: gráficas que parecen acantilados.

La CPU estaba al máximo, la cola de escucha de PHP-FPM subía y la página principal iba lenta. La primera suposición fue la clásica: “El CDN debe estar caído”. No lo estaba. La tasa de aciertos de cache era excelente. Eso es lo que hizo confuso el incidente: las páginas públicas estaban cacheadas, así que ¿por qué PHP se estaba muriendo?

La respuesta estaba en los logs de acceso. Una botnet estaba golpeando /wp-json/wp/v2/users y /wp-json/wp/v2/posts con tamaños de página grandes y parámetros de búsqueda. El CDN los reenviaba felizmente porque las peticiones parecían “GET normales”. El equipo había asumido “GET es seguro” y “CDN == escudo”. Ninguno es verdad.

La solución fue aburrida y efectiva: limitar la tasa de los endpoints REST en el borde, añadir cabeceras de cache más estrictas donde sea seguro y bloquear endpoints de enumeración de usuarios. También endurecieron la API para limitar per_page y desactivaron rutas innecesarias. La CPU bajó al instante y la lección quedó clara: la superficie de ataque incluye todos los endpoints dinámicos de lectura, no solo los formularios de login.

Micro-historia 2: La optimización que salió mal

Otra organización tenía picos de CPU recurrentes durante lanzamientos de campañas. Alguien propuso una “ganancia simple”: aumentar pm.max_children de PHP-FPM y las conexiones de MySQL para que el servidor “soportara más concurrencia”. Sonaba razonable. Las gráficas mejoraron por alrededor de una hora.

Luego el sitio se volvió más lento. No solo más lento—errático. Algunas peticiones respondían rápido; otras tardaban una eternidad. La CPU seguía alta, el load average subió y MySQL empezó a mostrar tiempos de consulta largos. Eventualmente la máquina comenzó a hacer swap y el kernel empezó a matar procesos. El equipo había escalado su cuello de botella hasta un colapso total del sistema.

Causa raíz: aumentaron la concurrencia de PHP sin reducir el costo por petición. Más workers significaron más consultas costosas simultáneas, que saturaron el buffer pool de la base de datos. La presión de memoria adicional llevó al sistema a hacer swap, convirtiendo un “problema de CPU” en “problema de todo”.

La solución real fue menos glamurosa: limitar los workers de PHP para que quepan en RAM, habilitar slowlog de PHP-FPM, identificar el plugin que generaba consultas de productos sin índices y cambiar su comportamiento. Después de eso, pudieron aumentar concurrencia con cuidado. Aprendieron a la mala que “más workers” no es optimización; es un multiplicador.

Micro-historia 3: La práctica aburrida pero correcta que salvó el día

Una marca global tenía una instancia de WordPress soportando un gran evento de prensa. Ya les había pasado antes, así que siguieron un régimen soso: inventario semanal de plugins, actualizaciones en staging, pruebas de rendimiento base y un runbook estándar para picos de tráfico. Nada heroico, solo hábitos.

El día del evento, la CPU subió como se esperaba. Luego subió más. No se impacientaron. El runbook comenzaba con: identificar endpoint caliente, IPs principales, cola PHP-FPM, consultas lentas de MySQL. En minutos encontraron una oleada de peticiones a un endpoint de búsqueda que eludía el cache de página. Era tráfico real de usuarios, no bots.

Porque tenían mitigaciones preconstruidas, activaron una capa de resultados de búsqueda cacheada (TTL corto), redujeron temporalmente la complejidad de la búsqueda (menos campos) y establecieron un límite de tasa sensato para patrones abusivos. El sitio se mantuvo en pie. Nadie notó el compromiso salvo el equipo que miraba las gráficas.

La práctica que salvó no fue una herramienta sofisticada. Fue tener instrumentación y un flujo de trabajo predecible antes de la crisis. “Aburrido” es lo que llamas a la fiabilidad cuando funciona.

Puntos calientes comunes de CPU en WordPress (lo que suele ser culpable)

1) wp-login.php y credential stuffing

CPU alta junto con muchas POSTs a wp-login.php es casi siempre fuerza bruta o credential stuffing. Incluso los logins fallidos cuestan CPU: hashing de contraseñas, manejo de sesiones y hooks de plugins que se ejecutan en intentos de autenticación.

Qué hacer: aplica límites de tasa, añade desafíos para bots en el borde, desactiva enumeración de usuarios y considera mover el admin detrás de una VPN o listas de permitidos si la organización puede tolerarlo.

2) xmlrpc.php (pingback + fuerza bruta)

XML-RPC puede ser abusado tanto para intentos de login como para amplificación por pingback. Muchos sitios no lo necesitan. Desactivarlo suele reducir la superficie de ataque de forma considerable.

3) admin-ajax.php (sondeos y fragmentos)

Admin AJAX es la fuente #1 de CPU por plugins. Scripts del front-end sondean para actualizaciones (widgets de chat, constructores de páginas, analítica), WooCommerce refresca fragmentos y algunos plugins usan admin-ajax como su API porque está disponible.

Señal: tasa de peticiones enorme, endpoint constante, muchas IPs y el slowlog muestra funciones de plugins.

4) Endpoints REST con consultas no acotadas

Los endpoints REST pueden ser golpeados por scrapers o por tu propio front-end si trabajas en headless. Si permites per_page grande, filtros costosos o consultas profundas en meta, has construido una trituradora de CPU con una interfaz agradable.

5) Búsqueda y filtros (consultas LIKE, meta queries)

La búsqueda en WordPress es notoriamente cara en datasets grandes, especialmente con WooCommerce y campos personalizados. Las meta queries sobre tablas sin índices son un desastre en cámara lenta.

6) Tormentas de wp-cron

Cron disparado por tráfico significa que los picos pueden causar más ejecuciones de cron, que generan más CPU, que ralentiza el sitio, que incrementa la superposición. Es un bucle de retroalimentación con nombre educado.

7) Plugins “de seguridad” que hacen escaneo por petición

Algunos plugins de seguridad inspeccionan cada petición en profundidad (regex, chequeos de archivos, llamadas a reputación de IP). Pueden ser útiles, pero también pueden convertirse en tu mayor consumidor de CPU. Mídelos como cualquier otro plugin.

Mitigaciones que funcionan (y las que causan problemas después)

Bloquea y limita en el borde primero

Si el tráfico hostil forma parte de la historia, no “lo arregles en PHP”. PHP es la capa equivocada para abuso volumétrico. Usa un WAF/CDN, limitación de tasa en Nginx o reglas de firewall.

Ejemplos de limitación de tasa en Nginx

Si controlas Nginx, puedes racionar endpoints abusivos. La meta no es castigar a usuarios legítimos. Es evitar que un actor consuma todos los workers.

cr0x@server:~$ sudo nginx -T | grep -n "limit_req_zone" | head
53:limit_req_zone $binary_remote_addr zone=wp_login:10m rate=5r/m;
54:limit_req_zone $binary_remote_addr zone=wp_ajax:10m rate=30r/m;

Significado: Define tasas por IP. Login debería ser lento; AJAX puede ser más alto pero no infinito.

Decisión: Aplica esto a las ubicaciones correctas, recarga Nginx y observa las tasas 429.

cr0x@server:~$ sudo grep -n "location = /wp-login.php" -n /etc/nginx/sites-enabled/default
121:location = /wp-login.php {
122:    limit_req zone=wp_login burst=10 nodelay;
123:    include snippets/fastcgi-php.conf;
124:    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
125:}

Significado: Los intentos de login están limitados. Los bursts permiten picos cortos; el abuso sostenido recibe 429.

Decisión: Si recibes quejas de usuarios, afloja el burst ligeramente pero mantén la tasa baja. La fuerza bruta es paciente.

Haz wp-cron predecible

Desactiva WP-Cron disparado por tráfico y ejecútalo con un cron real. Este es uno de esos cambios que reduce los picos “misteriosos”.

cr0x@server:~$ sudo -u www-data wp config set DISABLE_WP_CRON true --raw
Success: Updated the constant 'DISABLE_WP_CRON' in the 'wp-config.php' file.

Significado: WordPress deja de invocar cron en las cargas de página.

Decisión: Añade una entrada de cron del sistema para ejecutar wp-cron.php a intervalos sensatos.

cr0x@server:~$ sudo crontab -u www-data -l | tail -n 3
*/5 * * * * cd /var/www/html && wp cron event run --due-now >/dev/null 2>&1

Significado: El cron ahora es impulsado por tiempo, no por tráfico.

Decisión: Si tienes mucha carga de fondo (acciones de WooCommerce), ajusta el intervalo o usa runers de Action Scheduler con más cuidado.

Usa cache de verdad (pero no finjas que lo cubre todo)

El cache de página completo es excelente para contenido anónimo. El cache de objetos es excelente para consultas repetidas a BD. Ninguno hace que el checkout de WooCommerce sea baratísimo de manera mágica. Lo que sí hacen es reducir trabajo repetido para que la CPU se gaste en acciones reales de usuarios, no en consultas repetidas por el mismo menú.

Desconfía de plugins “cache todo” que añaden capas sin observabilidad. Si no puedes medir la tasa de aciertos y patrones de expulsión, estás operando un rumor.

Dimensiona PHP-FPM correctamente (no lo pongas a “infinito”)

Los picos de CPU a menudo empiezan como peticiones lentas y luego se convierten en cola. Si fijas pm.max_children demasiado bajo, haces cola. Demasiado alto, te quedas sin RAM y haces swap. Hay un punto dulce que depende del consumo medio de memoria por worker PHP.

cr0x@server:~$ ps --no-headers -o rss -C php-fpm8.2 | awk '{sum+=$1; n++} END{print "avg_rss_kb=" int(sum/n) ", workers=" n}'
avg_rss_kb=89234, workers=24

Significado: El RSS medio por worker es ~87MB. Multiplícalo por max children y añade overhead para MySQL, cache OS y Nginx.

Decisión: Si tienes 2GB de RAM, 24 workers a 87MB ya son ~2.1GB, lo cual no es posible sin swap. Límitalo más, luego reduce el uso de memoria por petición.

Errores comunes: síntoma → causa raíz → solución

Esta es la sección donde reconoces tu propio incidente. Está bien. Todos hemos pasado por eso. La diferencia es si lo anotas y dejas de repetirlo.

1) Síntoma: CPU 100%, “load average” enorme; el sitio se agota

Causa raíz: Cola de escucha de PHP-FPM creciendo porque las peticiones son lentas, no porque necesites más workers.

Solución: Activa PHP-FPM slowlog y atribuye rutas lentas; bloquea/limita endpoints abusivos; reduce trabajo por petición; luego ajusta max children según RAM.

2) Síntoma: CPU alta tras activar un plugin de cache

Causa raíz: Warmup o precarga de cache golpeando cada URL; o misses de cache que causan avalanchas bajo concurrencia.

Solución: Desactiva el warmup agresivo en producción; añade bloqueo de cache si está soportado; reduce la concurrencia de los warmers; asegura que el cache de objetos esté bien configurado.

3) Síntoma: picos cada pocos minutos como un reloj

Causa raíz: WP-Cron o Action Scheduler ejecutando tareas pesadas, disparadas por tráfico o con programación incorrecta.

Solución: Desactiva WP-Cron y ejecuta un cron real; inspecciona eventos programados; arregla tareas que hagan demasiado por ejecución.

4) Síntoma: muchas peticiones a admin-ajax; CPU se derrite con tráfico “normal”

Causa raíz: Refresh de fragments de WooCommerce, assets del editor de constructores de páginas o sondeos front-end (heartbeat, chat) generando llamadas sin cache de alta frecuencia.

Solución: Reduce la frecuencia; desactiva fragments donde sea seguro; cachea respuestas; asegúrate de que estos endpoints no sean golpeados por usuarios anónimos innecesariamente.

5) Síntoma: MySQL con CPU alta; PHP moderado; consultas muestran “Sending data”

Causa raíz: consultas sin índices (meta queries, búsquedas LIKE), tablas grandes o sorts costosos.

Solución: habilita slow query log; identifica la fuente de la consulta; añade índices con cuidado; cambia configuraciones de plugins para evitar las peores consultas; añade caché de objetos.

6) Síntoma: CPU alta solo durante crawls; las páginas son mayormente cacheables

Causa raíz: bots que evaden cache mediante parámetros de consulta, cabeceras o golpeando endpoints no cacheados como sitemaps y feeds.

Solución: cachea sitemaps; configura reglas de cache para patrones comunes de bots; limita la tasa; bloquea user agents abusivos que ignoran robots.

7) Síntoma: errores 502/504 aleatorios aparecen cuando la CPU sube

Causa raíz: timeouts upstream (Nginx/Apache esperando a PHP-FPM), workers insuficientes o workers bloqueados en llamadas lentas a BD/remotas.

Solución: correlaciona logs de errores con el slowlog de PHP-FPM; aumenta timeouts solo después de reducir la duración de peticiones; evita enmascarar un backend lento aumentando timeouts para siempre.

Listas de verificación / plan paso a paso

Paso a paso: encuentra el bot o plugin que golpea en 30–60 minutos

  1. Verifica que la CPU sea real: revisa mpstat para steal e iowait.
  2. Identifica el proceso más caliente: ps ordenado por CPU. Usualmente PHP-FPM.
  3. Revisa la cola de PHP: página de estado de FPM; confirma crecimiento de la cola.
  4. Encuentra los endpoints principales: parsea logs de acceso para las rutas URL más frecuentes.
  5. Encuentra los clientes principales: top IPs para endpoints calientes. Decide bloqueos/limites ahora.
  6. Habilita PHP-FPM slowlog (ventana corta). Captura backtraces.
  7. Mapea slowlog a plugin/tema: rutas de archivos bajo wp-content/plugins o directorios de tema.
  8. Confirma con una prueba controlada: desactiva temporalmente el plugin vía WP-CLI o flag de feature; observa CPU y latencia.
  9. Si la BD es sospechosa: revisa processlist y slow query log; mapea consultas a endpoints.
  10. Aplica una solución duradera: reemplazo/config del plugin, cache, límites de tasa, cambios de cron, optimización de consultas.
  11. Escribe una nota de runbook: qué endpoint, qué plugin, qué mitigación, qué métrica confirma la salud.

Checklist: mitigaciones seguras que puedes aplicar durante un incidente

  • Limitar la tasa de wp-login.php, xmlrpc.php y rutas /wp-json/ abusivas.
  • Bloquear IPs claramente malas (pero prefiere límites de tasa sobre bloqueos manuales tipo whack-a-mole).
  • Habilitar PHP-FPM slowlog temporalmente; recopilar evidencia.
  • Escalar verticalmente solo si steal es bajo y tienes evidencia de que estás limitado por CPU (no I/O o bloqueo de BD).
  • Limitar workers de PHP-FPM para evitar muerte por swap.
  • Desactivar WP-Cron y ejecutar un cron real.
  • Desactivar el plugin específico que genera trazas en slowlog si el sitio lo tolera.
  • Apagar funciones no esenciales y costosas (productos relacionados, búsqueda en vivo, filtros sofisticados) por la duración del incidente.

Checklist: cambios para programar después del incidente

  • Mantén una línea base: tasa de peticiones, p95 de latencia, cola de PHP-FPM, endpoints principales.
  • Implementa logs de acceso estructurados que incluyan tiempo upstream.
  • Mantén disponible el slow query log (aunque normalmente esté apagado) y sabe cómo activarlo rápido.
  • Establece un “presupuesto de rendimiento para plugins”: evita plugins que hagan trabajo intensivo en cada petición.
  • Crea una cadencia de actualizaciones con staging y plan de rollback.

Preguntas frecuentes

1) ¿Cómo sé si son bots o usuarios reales?

Mira la concentración por IP, user agent y comportamiento. Los bots suelen golpear el mismo endpoint a altas tasas, ignoran cookies y tienen baja diversidad de páginas. Los usuarios reales navegan.

2) ¿Debería simplemente añadir más núcleos de CPU?

Solo después de haber atribuido el trabajo. Escalar puede comprar tiempo, pero también aumenta la factura y puede amplificar los cuellos de botella de BD. Si un endpoint es abusivo, bloquéalo primero.

3) ¿admin-ajax.php es siempre malo?

No. Es un mecanismo legítimo. Se vuelve malo cuando se usa como API de alta frecuencia para usuarios anónimos o cuando las respuestas disparan consultas DB costosas.

4) ¿Cuál es la forma más rápida de encontrar el plugin que causa carga?

Las trazas del slowlog de PHP-FPM son el método más rápido y fiable. Los logs de acceso te dicen qué está caliente; el slowlog te dice qué código es lento.

5) Mi CPU está alta pero PHP-FPM no lo está. ¿Qué entonces?

Revisa la CPU de MySQL y el processlist. También revisa iowait y uso de swap. A veces es indexación de búsqueda, procesamiento de imágenes, trabajos de backup o incluso otro inquilino en el mismo host.

6) ¿El cache puede arreglar picos de CPU en WooCommerce?

Parcialmente. Puedes cachear páginas anónimas y algunos fragmentos, pero carrito/checkout y comportamiento autenticado seguirán siendo dinámicos. Concéntrate en reducir el ruido de admin-ajax y consultas costosas.

7) ¿Debo desactivar xmlrpc.php?

Si no lo usas (la mayoría de sitios no lo hacen), sí—desactívalo o bloquéalo. Si debes mantenerlo, limita su tasa fuertemente y monitorea intentos de login.

8) ¿Redis como caché de objetos siempre es una ganancia?

Es ventaja cuando reduce lecturas repetidas a BD y tienes un cache estable. Puede salir mal si enmascara código ineficiente hasta que ocurren tormentas de expulsión o si una mala configuración provoca misses constantes.

9) ¿Por qué los picos de CPU se correlacionan con cron?

Porque WP-Cron puede dispararse en cargas de página, lo que crea bucles de retroalimentación bajo tráfico. Pasarlo a un cron real convierte picos sorpresa en trabajo programado.

10) ¿Cuál es la causa más común de WordPress al 100% de CPU?

Alto volumen de peticiones a un endpoint dinámico que elude cache, combinado con código de plugin lento o consultas DB costosas. Rara vez es solo “el core de WordPress”.

Conclusión: próximos pasos que puedes hacer hoy

Si tu sitio WordPress está consumiendo la CPU al máximo, no lo trates como un misterio. Trátalo como una investigación basada en evidencia.

  1. Ejecuta el guion de diagnóstico rápido: identifica proceso → endpoint → cliente → ruta de código.
  2. Activa el PHP-FPM slowlog por una ventana corta y captura backtraces durante el pico.
  3. Analiza los logs de acceso para encontrar URLs e IPs principales. Limita la tasa en endpoints abusivos de inmediato.
  4. Mapea trazas de pila a plugins y desactiva/reemplaza a los responsables con pruebas controladas.
  5. Haz el cron predecible y dimensiona PHP-FPM para evitar colas y muerte por swap.
  6. Escribe lo que aprendiste: el endpoint, el plugin, la mitigación y la métrica que demuestra que está arreglado. Tu yo futuro te lo agradecerá.

La CPU no es una falla moral. Es una factura que estás pagando en tiempo real. Obtén el recibo: la petición, el plugin, la consulta, el cliente. Luego haz que pare.

← Anterior
ARC de correo electrónico explicado (la versión corta y útil) — cuándo ayuda el reenvío de correo
Siguiente →
Pantallas esqueleto con CSS puro: shimmer, movimiento reducido y rendimiento

Deja un comentario