El problema: un portal de noticias al límite
El rendimiento no es un lujo, es una necesidad. Me contactó el responsable de un portal de noticias construido sobre WordPress que sufría una lentitud crítica. El tiempo de respuesta inicial (TTFB) rondaba los 11,9 segundos en horas de tráfico normal, con picos que dejaban la web inaccesible.
Los síntomas eran claros: pantallas en blanco intermitentes, errores 500 aleatorios en operaciones de WooCommerce y una portada que tardaba casi medio minuto en servirse en momentos de tráfico alto. Google ya había comenzado a penalizar el sitio en sus métricas de Core Web Vitals.
El sitio contaba con varios años de historia, múltiples plugins acumulados y un tema personalizado con lógica de consultas propia. La instalación de WordPress había crecido de forma orgánica sin que ningún desarrollador hubiera revisado el rendimiento en profundidad desde su lanzamiento. Esta situación —muy habitual en portales con redacciones activas— genera una deuda técnica invisible que no aparece hasta que el tráfico crece o se actualiza algún plugin clave. El cliente había probado ya tres plugins de caché distintos sin éxito, lo que es una señal inequívoca de que el problema estaba en la generación del HTML, no en la entrega.
La mayoría de los tutoriales dicen "instala WP Rocket y listo". Pero cuando el problema está en la generación del HTML —es decir, en el código PHP que corre en el servidor antes de que exista algo que cachear—, ningún plugin de caché te salvará. Primero hay que diagnosticar.
1. El diagnóstico: ingeniería inversa del retraso
Para no trabajar a ciegas, lo primero fue implementar un profiler personalizado como mu-plugin. A diferencia de herramientas genéricas, este profiler se engancha directamente en los hooks de WordPress y registra el tiempo exacto consumido en cada etapa del ciclo de carga.
// /wp-content/mu-plugins/profiler.php add_action('muplugins_loaded', function() { $GLOBALS['_prof'] = ['start' => microtime(true)]; }); add_action('setup_theme', function() { $GLOBALS['_prof']['setup_theme'] = microtime(true); }); add_action('wp', function() { $elapsed = microtime(true) - $GLOBALS['_prof']['setup_theme']; // Resultado: 6,3 segundos solo en setup_theme → loop error_log('[PROF] setup_theme→wp: ' . round($elapsed, 4) . 's'); });
Descubrimiento crítico: el hook setup_theme y la primera ejecución del loop de noticias estaban bloqueando el hilo de ejecución durante más de 6,3 segundos. La causa: múltiples llamadas a Pods::find() sin límite de resultados y con cláusulas WHERE que impedían el uso de índices en MySQL. El problema no era el servidor, era la arquitectura de las consultas.
La diferencia entre Pods::find() mal configurado y una consulta WP_Query optimizada es abismal. Mientras que Pods genera internamente varias subconsultas para resolver relaciones entre tipos de contenido, WP_Query trabaja directamente sobre las tablas nativas de WordPress (wp_posts y wp_postmeta), que tienen índices correctamente definidos. Al reescribir las ocho consultas de la portada usando WP_Query con 'no_found_rows' => true y 'update_post_meta_cache' => false, eliminamos la sobrecarga innecesaria de calcular paginación y cargar metadatos que no se usaban. El tiempo de estas consultas pasó de más de 5 segundos a menos de 80ms en total.
2. Estabilización de la infraestructura PHP
Durante el diagnóstico detectamos un segundo problema: los límites de memoria definidos en wp-config.php estaban siendo ignorados por la configuración del servidor. El valor de WP_MEMORY_LIMIT y WP_MAX_MEMORY_LIMIT era correcto en WordPress, pero phpinfo() mostraba solo 128M disponibles.
# /public_html/.user.ini memory_limit = 512M max_execution_time = 300 upload_max_filesize = 64M post_max_size = 64M
- Acción: Forzamos un límite de 512M mediante
.user.ini, que tiene precedencia sobrephp.inidel servidor en entornos compartidos con Plesk y cPanel. - Resultado: Eliminación total de los errores por falta de memoria (Allowed memory size exhausted) que interrumpían los procesos de WooCommerce.
Este tipo de discrepancia entre lo que WordPress cree que tiene disponible y lo que PHP realmente asigna es más frecuente de lo que parece en hostings compartidos. El archivo .user.ini es el mecanismo estándar para sobrescribir directivas de PHP a nivel de directorio sin necesidad de acceso root al servidor. Su ventaja sobre modificar directamente php.ini es que el hosting no lo sobrescribe en actualizaciones del servidor. Es importante recordar que los cambios en .user.ini no son inmediatos: PHP-FPM los recarga cada cierto tiempo (configurable con user_ini.cache_ttl), por lo que puede ser necesario reiniciar el proceso o esperar unos minutos antes de verificar los cambios con phpinfo().
3. Resolución de conflictos en la base de datos: Action Scheduler colapsado
El sistema de tareas programadas (Action Scheduler), usado internamente por WooCommerce, estaba en un estado crítico. Más de 47.000 tareas fallidas o "claimed" se habían acumulado en las tablas wp_actionscheduler_actions y wp_actionscheduler_claims, provocando bloqueos de tabla que afectaban a todas las consultas del sitio.
El error mysqli_sql_exception: Commands out of sync; you can't run this command now en MySQL no aparece en los tutoriales de WordPress, pero es el síntoma clásico de un Action Scheduler colapsado. La conexión MySQL queda en un estado inconsistente porque una transacción anterior no se completó correctamente.
-- 1. Liberar todas las claims bloqueadas DELETE FROM wp_actionscheduler_claims WHERE date_created_gmt < DATE_SUB(NOW(), INTERVAL 1 HOUR); -- 2. Marcar tareas fallidas antiguas como canceladas UPDATE wp_actionscheduler_actions SET status = 'canceled' WHERE status = 'failed' AND scheduled_date_gmt < DATE_SUB(NOW(), INTERVAL 7 DAY); -- 3. Optimizar las tablas afectadas OPTIMIZE TABLE wp_actionscheduler_actions, wp_actionscheduler_claims;
Una vez ejecutadas las sentencias SQL de limpieza, el comportamiento del sitio mejoró notablemente incluso antes de aplicar la caché de fragmentos. Los bloqueos de tabla desaparecieron porque MySQL ya no tenía que gestionar miles de filas en estado inconsistente cada vez que WooCommerce intentaba procesar una tarea nueva. Para evitar que el problema se repita, configuramos WooCommerce para que el Action Scheduler solo intente procesar un máximo de 5 tareas concurrentes y habilitamos el Cleanup automático de tareas completadas con más de 30 días de antigüedad, una opción disponible desde el panel de WooCommerce > Status > Action Scheduler.
4. Estrategia de caché avanzada: Transients API + output buffering
Con la infraestructura estabilizada, abordamos el problema de raíz: la arquitectura de la portada. El template realizaba 8 consultas independientes de Pods para construir los módulos de noticias (últimas, destacadas, por categoría, sin repetición), cada una con lógica de exclusión de IDs ya mostrados.
Ningún plugin de caché de página puede resolver esto eficientemente porque el contenido cambia con cada publicación. La solución correcta es una caché de fragmentos a nivel de código.
/** * Caché de fragmentos para la portada de noticias. * Se invalida automáticamente al publicar o actualizar posts. */ function lj_render_portada_cached() { $cache_key = 'portada_noticias_v1'; $cached = get_transient( $cache_key ); if ( false !== $cached ) { echo $cached; // Servir desde caché: ~0ms return; } // Capturar el HTML de la portada con output buffering ob_start(); lj_render_portada_real(); // Las 8 consultas costosas $html = ob_get_clean(); // Guardar en transient: expira en 5 minutos set_transient( $cache_key, $html, 5 * MINUTE_IN_SECONDS ); echo $html; } // Invalidar caché al publicar o actualizar add_action( 'save_post', function() { delete_transient( 'portada_noticias_v1' ); });
- Primera carga: un usuario genera el HTML completo (8 consultas). Se guarda el resultado en la base de datos como transient.
- Cargas siguientes: todos los usuarios reciben el HTML ya renderizado directamente desde MySQL o desde un backend de caché como Redis/Memcached.
- WP Super Cache: configurado como segunda capa para servir páginas estáticas en el resto del sitio, reduciendo aún más la carga del servidor.
WP Rocket y similares cachean la página completa, lo que genera problemas con contenido dinámico (usuario logueado, carrito de WooCommerce, posts en borradores). La caché de fragmentos solo cachea el bloque pesado y deja el resto dinámico, dando lo mejor de ambos mundos.
El resultado: velocidad extrema y web estable
Tras la intervención completa, los resultados superaron las expectativas del cliente:
- TTFB antes: 11,9 segundos de tiempo de respuesta inicial.
- TTFB después: 0,03 segundos (30ms) — mejora del 99,7 %.
- Errores 500: eliminados por completo. Cero caídas en los 30 días posteriores.
- Core Web Vitals: LCP pasó de "Deficiente" a "Bueno" en Google Search Console.
- Estabilidad: 100 % de uptime, carga instantánea y experiencia de usuario premium.
Más allá de los números, el impacto real se tradujo en una experiencia de usuario completamente diferente. Los lectores del portal podían navegar entre secciones sin esperas, las páginas de artículos cargaban de forma instantánea y el equipo de redacción dejó de recibir avisos de caída del sitio durante los momentos de mayor tráfico —habitualmente tras publicar noticias de última hora. La mejora en Core Web Vitals también tuvo efecto en el posicionamiento orgánico: en los dos meses siguientes a la intervención, el sitio recuperó posiciones en Google para varias de sus palabras clave principales.
Este caso ilustra una lección que se repite con frecuencia: la mayoría de los problemas graves de rendimiento en WordPress no tienen solución con plugins, sino con ingeniería. Diagnosticar antes de actuar, entender el ciclo de ejecución de PHP y WordPress, y aplicar las soluciones en la capa correcta —base de datos, infraestructura PHP, arquitectura de caché— es lo que marca la diferencia entre un parche temporal y una web que funciona bien durante años.
Resumen del proceso de intervención
- Profiler personalizado como mu-plugin para identificar exactamente qué hook consumía los 6+ segundos.
- Corrección de límites PHP mediante
.user.inipara eliminar errores de memoria en WooCommerce. - Limpieza quirúrgica de Action Scheduler: liberación de claims bloqueados y cancelación de 47.000 tareas fallidas.
- Refactoring de consultas Pods: reescritas con
WP_Querynativo para aprovechar los índices de MySQL. - Caché de fragmentos con
Transients API+ob_start()para la portada dinámica. - WP Super Cache como capa de caché estática para el resto del sitio.
Preguntas frecuentes sobre optimización de WordPress
WP Rocket y similares solo pueden cachear lo que el servidor ya ha generado. Si el problema está en la generación del HTML —consultas SQL lentas, plugins mal codificados, Action Scheduler colapsado— ningún plugin de caché lo puede resolver. Primero hay que diagnosticar con un profiler qué hook o consulta está bloqueando la ejecución de PHP.
El TTFB (Time To First Byte) es el tiempo que tarda el servidor en enviar el primer byte de respuesta. Google considera menos de 800ms como "Bueno" en Core Web Vitals. Para WordPress con caché activa, un TTFB por debajo de 200ms es alcanzable. Por encima de 2 segundos ya es problemático para el SEO y la experiencia de usuario.
Ve a tu base de datos con phpMyAdmin o mediante WP-CLI y ejecuta: SELECT status, COUNT(*) FROM wp_actionscheduler_actions GROUP BY status;. Si ves miles de tareas en estado "failed" o "in-progress" (claimed) con fechas antiguas, el Action Scheduler está colapsado y está generando bloqueos de tabla en MySQL que ralentizan todo el sitio.
Sí, y es la forma recomendada por WordPress para cachear datos temporales. Por defecto usa la base de datos como backend, pero si hay un plugin de object cache como Redis Object Cache o W3 Total Cache con Memcached, los transients se almacenan automáticamente en memoria, haciendo las lecturas prácticamente instantáneas (<1ms).
¿Tu WordPress no rinde como debería?
Analizo tu sitio sin compromiso. Si el problema existe, te explico exactamente qué lo está causando y cómo lo solucionamos. No cobro por el diagnóstico inicial.
Solicitar diagnóstico WordPress →
💬 8 comentarios
Llevaba meses con el mismo problema y no entendía por qué WP Rocket no lo solucionaba. El query del Action Scheduler que propones me abrió los ojos: tenía 83.000 tareas en estado "failed". Después de limpiarlas, el TTFB bajó de 9 s a 400 ms en frío. No al nivel tuyo, pero ya es respirable. ¿Algún consejo para bajar ese último tramo?
Marcos, 400 ms en frío ya es un salto enorme, bien hecho. Para bajar más, lo siguiente que miraría es si alguna consulta sigue disparándose en
setup_themeowp_head. Añade el profiler que menciono en el artículo como mu-plugin y vuelca el log: casi siempre hay un plugin secundario con una consulta sin caché que explica esos 400 ms restantes. Si te atascas, escríbeme por el formulario de contacto.Artículo técnico de los buenos, sin relleno. La parte de la caché de fragmentos con
ob_start()+ Transients es exactamente lo que necesitaba para la portada de un cliente con Elementor que hace 3 queries pesadísimas. ¿Funciona bien aunque el tema sea de Elementor o hay algún conflicto con su sistema de caché interno?Sara, con Elementor funciona bien siempre que apliques el fragmento en un bloque de código PHP (require un plugin como "Code Snippets") o directamente en el
functions.phpdel child theme, enganchado a un shortcode o a una función que Elementor llame. Lo que hay que evitar es cachearlo si el usuario está logueado o tiene productos en el carrito (WooCommerce). Con unif (is_user_logged_in()) return;antes delob_start()vas segura.Leer esto fue el momento "ahá" que necesitaba. Tenía un hook
woocommerce_before_shop_loopcon una consulta Pods sin caché que se ejecutaba en cada página de producto. Tardaba 4 s de TTFB. Reescribí la consulta conWP_Querytal como describes y metí un transient de 1 hora. Ahora el TTFB está en 180 ms. El cliente no se lo creía.¿Tendrías el código completo del profiler como mu-plugin que usas para la diagnosis? El que muestro en el artículo es suficiente para detectar el hook, pero me gustaría ver si también loggas el tiempo de cada consulta SQL de ese hook.
Ana, buena pregunta. El profiler que uso internamente también captura las queries SQL combinando el hook timer con
$wpdb->num_queriesantes y después. Lo tengo documentado como un segundo mu-plugin complementario. Tengo pendiente publicarlo como artículo independiente con el código completo y ejemplos de output. En breve estará por aquí.Llegué aquí desde una búsqueda en Google de "Action Scheduler 50000 failed tareas". Ejecuté el query de diagnóstico y tenía exactamente 51.400 acciones fallidas en mi tienda WooCommerce. Después de limpiarlas con
DELETE FROM wp_actionscheduler_actions WHERE status = 'failed';y las "in-progress" con fecha antigua, la administración de WordPress que tardaba 30 segundos en cargar ahora va fluida. Un artículo que debería ser lectura obligatoria para cualquier administrador de WooCommerce.Mi caso era diferente: no era WooCommerce sino un portal con Pods muy grande. El TTFB no bajaba de 7 s a pesar de tener W3 Total Cache activo. Contacté con Luis por el formulario y en menos de 48 horas me había localizado el problema: tres campos relacionales de Pods sin
lazy loadingque se resolvían en cada carga de página aunque no se mostraran en esa vista. Migración aWP_Querycon meta queries y el sitio voló. Muy recomendable si quieres a alguien que realmente entiende lo que hay debajo del WordPress.