WordPress: ‘Tiempo máximo de ejecución excedido’ — por qué ocurre y soluciones seguras

¿Te fue útil?

Haces clic en “Actualizar plugin”. El indicador gira. Luego WordPress se cae con: “Fatal error: Maximum execution time of 30 seconds exceeded”. O la página queda en blanco y te quedas contemplando tu propio reflejo como si te debiera dinero.

Este error no es WordPress dramatizando. Es tu servidor aplicando un corte en seco: una petición PHP se ejecutó más tiempo del permitido. La clave es arreglarlo sin decir “simplemente ponlo a 300 segundos” y dejarlo así—porque los timeouts largos convierten problemas pequeños en incidentes de producción.

Qué significa realmente el error (y qué no significa)

En la mayoría de pilas de WordPress, “Max execution time exceeded” lo lanza PHP cuando una petición se ejecuta más tiempo que max_execution_time. Ese es un límite de PHP (por petición) diseñado para evitar que código descontrolado acapare CPU indefinidamente.

Pero en sistemas reales, el propio límite de PHP rara vez es el único timeout en juego. Normalmente tienes una pila de temporizadores:

  • Timeout del navegador/cliente (el usuario se rinde o el proxy inverso cierra la conexión).
  • Timeouts del proxy inverso (Nginx, CDN, balanceador de carga).
  • Timeouts del servidor web (Apache, Nginx fastcgi).
  • Timeouts de PHP-FPM (request_terminate_timeout o ajustes del pool).
  • Límite del script PHP (max_execution_time).
  • Timeouts de la base de datos (MySQL wait timeout, lock wait timeout).
  • Timeouts de API externas (pasarelas de pago, SMTP, servidores de licencias).

Así que cuando ves el error, no asumas “sube max_execution_time y asunto resuelto.” A veces PHP es solo el mensajero. El verdadero culpable puede ser una consulta de base de datos lenta, tablas bloqueadas, CPU sobrecargada, I/O de disco saturado o un plugin haciendo algo cuestionable como redimensionar 200 imágenes en una sola petición web.

Toma de posición: trata los errores de tiempo máximo como alarmas de humo. Puedes quitar las pilas, claro. Pero quizá deberías comprobar por qué la cocina está en llamas.

Una cita para tener cerca

“La esperanza no es una estrategia.” — Gene Kranz

Sí, es popular en círculos de operaciones. También es exacta. Adivinar los timeouts es esperanza. Medir es estrategia.

Por qué ocurre: los verdaderos cuellos de botella

1) Agotamiento de CPU: PHP esperando su turno

PHP no es inherentemente lento, pero es sensible a la contención. Si tu servidor tiene demasiados workers de PHP-FPM, cada uno obtiene una porción de CPU. Una tarea que “normalmente lleva 2 segundos” ahora tarda 45. Boom: timeout.

Esto es común en hosting compartido, VPS infra-dimensionados y entornos “redujimos la instancia para ahorrar dinero”.

2) I/O de disco: el asesino silencioso

WordPress es conversador: lee archivos PHP, carga plugins, escribe sesiones, actualiza opciones, genera miniaturas. Si el almacenamiento es lento (volúmenes adjuntos en red, discos sobre suscripción, RAID degradado, créditos de burst agotados), PHP puede pasar la mayor parte de su “tiempo de ejecución” bloqueado en I/O.

3) Cuellos de botella en la base de datos: consultas lentas, bloqueos y la realidad del vacuum

Cuando WordPress se queda colgado, a menudo está esperando MySQL/MariaDB. Causas clásicas:

  • Faltan índices introducidos por tablas de plugins.
  • Un wp_options enorme con filas autoloaded infladas.
  • Contención de locks durante importaciones, actualizaciones por lotes o tareas cron mal comportadas.
  • ALTER TABLE de larga duración durante horario laboral. Movimiento audaz.

4) Llamadas externas: tu sitio es tan rápido como esa API de terceros

Pasarelas de pago, calculadoras de envío, comprobaciones de licencia, servicios de optimización de imágenes, proveedores de correo—si un plugin hace llamadas HTTP síncronas durante la carga de página o acciones de administrador, hereda toda la fragilidad de Internet.

5) WP-Cron: “no es realmente cron” significa “a veces se acumula”

WP-Cron se ejecuta en cargas de página a menos que lo desactives y uses un cron real. Si el tráfico es bajo, los jobs se ejecutan con retraso y luego se amontonan. Si el tráfico es alto, los jobs pueden ejecutarse con demasiada frecuencia, solaparse y competir por recursos. En cualquier caso, las tareas cron largas ejecutadas vía peticiones HTTP son candidatas a timeouts.

6) Presión de memoria enmascarada como timeouts

A veces la petición es lenta porque está haciendo thrash de memoria: asignando, usando swap, disparando GC o fallando repetidamente al asignar suficiente memoria para procesar imágenes. Puede seguir apareciendo como un timeout de ejecución aunque el problema subyacente sea de memoria.

Broma corta #1: Los timeouts son como los plazos—todo el mundo los ignora hasta que el jefe los aplica.

Guía de diagnóstico rápido (verificar primero/segundo/tercero)

Si quieres encontrar el cuello de botella rápidamente, hazlo en este orden. Minimiza las conjeturas y maximiza la “prueba”.

Primero: identifica qué timeout se disparó (PHP vs PHP-FPM vs servidor web)

  • Si ves “Fatal error: Maximum execution time of X seconds exceeded” en logs o salida, probablemente se disparó el max_execution_time de PHP.
  • Si ves 502/504 en el proxy, sospecha de timeouts de Nginx/Apache/proxy o terminación de PHP-FPM.
  • Si ves líneas en los logs de PHP-FPM como “request terminated”, sospecha de request_terminate_timeout o de kill a nivel de pool.

Segundo: decide si es sistémico (recursos) o específico (un endpoint/Job)

  • Sistémico: CPU al máximo, carga promedio alta, latencia de disco alta, muchas peticiones lentas.
  • Específico: siempre ocurre al actualizar un plugin, al importar, en una página de administración concreta o en checkout.

Tercero: prueba la espera (CPU vs disco vs BD vs red)

  • CPU: cola de ejecución alta, muchos workers PHP, tiempo de pared largo con tiempo CPU real pequeño.
  • Disco: iowait alto, await lento en iostat.
  • BD: consultas lentas, esperas de bloqueo, hilos atascados en Sending data o Waiting for table metadata lock.
  • Red/externo: peticiones atascadas en llamadas HTTP; logs de plugins muestran timeouts remotos.

Cuarto: elige la solución segura más pequeña

La mayoría de las veces, la solución segura más pequeña no es “poner todo a 600 segundos.” Es una de:

  • Mover el trabajo pesado fuera de la petición web (WP-CLI, jobs en background).
  • Arreglar la consulta lenta / añadir índice / reducir el bloat de autoload.
  • Dimensionar correctamente los workers de PHP-FPM respecto a la CPU y memoria que realmente tienes.
  • Arreglar I/O de disco o migrar a un almacenamiento mejor.
  • Añadir timeouts y reintentos sensatos para llamadas externas, con fallback.

Tareas prácticas: comandos, salidas y decisiones (12+)

Estas son las tareas de “hazlo ahora, aprende algo”. Cada una incluye un comando, salida de ejemplo, qué significa y la decisión que tomas. Asume Linux con systemd, Nginx o Apache y PHP-FPM. Ajusta rutas según tu distro.

Task 1: Confirmar max_execution_time de PHP (baseline CLI)

cr0x@server:~$ php -i | grep -E '^max_execution_time'
max_execution_time => 30 => 30

Qué significa: PHP CLI está configurado a 30 segundos. La SAPI web puede diferir, pero esto te da una referencia.

Decisión: Si el CLI muestra un valor sensato (30–60) pero las peticiones web expiran de forma distinta, céntrate en las configuraciones de PHP-FPM y servidor web en lugar de adivinar php.ini global.

Task 2: Confirmar ajustes efectivos del pool de PHP-FPM

cr0x@server:~$ php-fpm8.2 -tt 2>/dev/null | head -n 20
[27-Dec-2025 10:11:02] NOTICE: configuration file /etc/php/8.2/fpm/php-fpm.conf test is successful
[27-Dec-2025 10:11:02] NOTICE: fpm is running, pid 1162
[27-Dec-2025 10:11:02] NOTICE: ready to handle connections

Qué significa: La config parsea. No es prueba de valores en tiempo de ejecución, pero evita sorpresas de “editaste el archivo equivocado”.

Decisión: Si la prueba de configuración falla, detente y corrige la sintaxis. Es muy común que los timeouts se deban a “el servicio no recargó” y eso es embarazoso.

Task 3: Comprobar timeouts FastCGI de Nginx

cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -E 'fastcgi_read_timeout|fastcgi_send_timeout|proxy_read_timeout' | head
fastcgi_read_timeout 60s;
fastcgi_send_timeout 60s;

Qué significa: Nginx esperará 60 segundos por la respuesta de PHP-FPM. Si PHP tiene permitido 120 segundos pero Nginx espera 60, verás 504s antes de que PHP termine.

Decisión: Alinea los timeouts. El timeout del borde/proxy debería ser generalmente un poco mayor que el de PHP para peticiones de cara al usuario, salvo que quieras imponer un límite de respuesta intencional.

Task 4: Comprobar timeout de Apache (si usas prefork/event con proxy_fcgi)

cr0x@server:~$ apachectl -S 2>/dev/null | head -n 5
VirtualHost configuration:
*:80                   is a NameVirtualHost
         default server example.com (/etc/apache2/sites-enabled/000-default.conf:1)
cr0x@server:~$ sudo apachectl -t -D DUMP_RUN_CFG 2>/dev/null | grep -E '^Timeout'
Timeout: 60

Qué significa: Apache espera 60 segundos. De nuevo: si PHP cree que puede ejecutarse 120 segundos, el cliente puede no verlo.

Decisión: O bajas PHP para que coincida y fuerzas el procesamiento en background, o aumentas el timeout web solo para endpoints específicos (importaciones) y mantienes el resto estricto.

Task 5: Encontrar el error exacto en logs de PHP-FPM y PHP

cr0x@server:~$ sudo grep -R "Maximum execution time" -n /var/log/php* /var/log/php8.2-fpm.log 2>/dev/null | tail -n 5
/var/log/php8.2-fpm.log:21944:PHP Fatal error:  Maximum execution time of 30 seconds exceeded in /var/www/html/wp-includes/http.php on line 320

Qué significa: El stack trace apunta a las solicitudes HTTP de WordPress. Normalmente es un plugin/tema haciendo una llamada externa, o el core de WordPress comunicándose consigo mismo.

Decisión: El siguiente paso es identificar qué plugin disparó la llamada (habilita el logging de WP debug brevemente, o reproduce con plugins desactivados).

Task 6: Observar presión de recursos en vivo (CPU, memoria, iowait)

cr0x@server:~$ top -b -n 1 | head -n 12
top - 10:13:44 up 12 days,  2:17,  1 user,  load average: 7.12, 6.95, 6.20
Tasks: 245 total,   2 running, 243 sleeping,   0 stopped,   0 zombie
%Cpu(s): 22.1 us,  3.2 sy,  0.0 ni, 63.4 id, 11.0 wa,  0.0 hi,  0.3 si,  0.0 st
MiB Mem :   7842.1 total,    412.6 free,   6120.3 used,   1309.2 buff/cache
MiB Swap:   2048.0 total,   1880.0 free,    168.0 used.   1200.0 avail Mem

Qué significa: wa en 11% sugiere espera de I/O de disco. La carga promedio es alta respecto a los núcleos de CPU (interpretar en consecuencia), lo que significa que se forman colas.

Decisión: No aumentes timeouts todavía. Investiga la latencia del almacenamiento y la concurrencia de PHP-FPM (demasiados workers pueden amplificar la contención de I/O).

Task 7: Medir latencia de disco con iostat

cr0x@server:~$ iostat -xz 1 3
Linux 6.1.0 (server) 	12/27/2025 	_x86_64_	(4 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          18.52    0.00    3.01   12.44    0.00   66.03

Device            r/s     w/s    rkB/s    wkB/s  avgrq-sz avgqu-sz   await  svctm  %util
nvme0n1         82.00  140.00  3200.0   8200.0     78.00     2.10   14.80   0.55  12.30

Qué significa: await en ~15ms no es catastrófico, pero para un WordPress concurrido puede acumularse. Si ves 50–200ms, eso es una alarma roja.

Decisión: Si await está consistentemente alto, arregla el almacenamiento (migrar a un volumen más rápido, parar vecinos ruidosos, investigar RAID/disco degradado) antes de tocar los timeouts de PHP.

Task 8: Comprobar saturación del pool PHP-FPM

cr0x@server:~$ sudo ss -s
Total: 1123 (kernel 0)
TCP:   317 (estab 54, closed 233, orphaned 0, timewait 233)

Transport Total     IP        IPv6
RAW	  0         0         0
UDP	  6         4         2
TCP	  84        47        37
INET	  90        51        39
FRAG	  0         0         0
cr0x@server:~$ sudo ps -o pid,pcpu,pmem,etime,cmd -C php-fpm8.2 --sort=-pcpu | head
  PID %CPU %MEM     ELAPSED CMD
 1422 18.2  2.7       01:03 php-fpm: pool www
 1418 15.9  2.6       01:01 php-fpm: pool www
 1399 12.4  2.5       00:59 php-fpm: pool www

Qué significa: Varios workers consumiendo CPU por más de un minuto sugiere peticiones lentas o deadlocks. Si todos están atascados en tiempos de elapsed similares, puede haber una estampida.

Decisión: Revisa la página de estado de PHP-FPM (si está habilitada) o los logs en busca de “server reached pm.max_children.” Si está saturado, reduce la concurrencia o añade CPU/memoria; no subas timeouts a lo loco.

Task 9: Inspeccionar MySQL para consultas lentas y esperas de bloqueo

cr0x@server:~$ mysql -e "SHOW PROCESSLIST\G" | head -n 30
*************************** 1. row ***************************
     Id: 27
   User: wpuser
   Host: localhost
     db: wordpress
Command: Query
   Time: 41
  State: Sending data
   Info: SELECT option_name, option_value FROM wp_options WHERE autoload='yes'
*************************** 2. row ***************************
     Id: 29
   User: wpuser
   Host: localhost
     db: wordpress
Command: Query
   Time: 39
  State: Waiting for table metadata lock
   Info: ALTER TABLE wp_posts ADD COLUMN foo int(11)

Qué significa: Una consulta es lenta (wp_options autoload). Otra sesión está bloqueada por un metadata lock debido a un cambio de esquema.

Decisión: No hagas cambios de esquema en medio de tráfico activo. También audita el bloat de wp_options autoload—esto es un clásico error autoinfligido en WordPress.

Task 10: Cuantificar el tamaño de opciones autoloaded

cr0x@server:~$ mysql -N -e "SELECT ROUND(SUM(LENGTH(option_value))/1024/1024,2) AS autoload_mb FROM wp_options WHERE autoload='yes';"
12.47

Qué significa: 12.47MB de opciones autoloaded es grande. WordPress carga esto en cada petición. Felicidades: has construido una pequeña base de datos de configuración dentro de tu base de datos de configuración.

Decisión: Identifica a los mayores culpables y cambia autoload a ‘no’ cuando sea seguro, o arregla el plugin. Si estás por encima de ~1–2MB, normalmente pagas un impuesto en cada vista de página.

Task 11: Encontrar los mayores culpables del autoload

cr0x@server:~$ mysql -e "SELECT option_name, ROUND(LENGTH(option_value)/1024,1) AS kb FROM wp_options WHERE autoload='yes' ORDER BY LENGTH(option_value) DESC LIMIT 10;"
+------------------------------+-------+
| option_name                  | kb    |
+------------------------------+-------+
| some_plugin_cache_blob       | 2048.3|
| another_plugin_settings      | 512.8 |
| rewrite_rules                | 256.4 |
| widget_custom_html           | 188.1 |
+------------------------------+-------+

Qué significa: Un plugin está guardando un blob de caché en autoload (malo), y otras opciones son voluminosas.

Decisión: Para blobs de caché: muévelos a un object cache o a transients con expiración; establece autoload ‘no’ si el plugin lo permite. No edites a mano sin backups y un plan de rollback.

Task 12: Comprobar salud y acumulación de WP-Cron con WP-CLI

cr0x@server:~$ cd /var/www/html && sudo -u www-data wp cron event list --fields=hook,next_run,recurrence --format=table | head
+------------------------------+---------------------+------------+
| hook                         | next_run            | recurrence |
+------------------------------+---------------------+------------+
| wp_version_check             | 2025-12-27 10:30:00 | twice_daily|
| woocommerce_cleanup_sessions | 2025-12-27 10:15:00 | hourly     |
| some_plugin_heavy_job        | 2025-12-27 10:01:00 | every_min  |
+------------------------------+---------------------+------------+

Qué significa: Un job que corre “every_min” es sospechoso en el mundo WordPress. Puede solaparse si dura más de un minuto.

Decisión: Si un hook es demasiado frecuente o pesado, muévelo a cron real, reduce la frecuencia o refactorízalo por lotes.

Task 13: Reproducir el endpoint lento con curl y timing

cr0x@server:~$ curl -s -o /dev/null -w "code=%{http_code} t_total=%{time_total} t_connect=%{time_connect} t_starttransfer=%{time_starttransfer}\n" https://example.com/wp-admin/admin-ajax.php?action=some_action
code=504 t_total=60.012 t_connect=0.012 t_starttransfer=60.001

Qué significa: Un corte duro a 60 segundos coincide con fastcgi_read_timeout de Nginx o timeout del proxy. El backend probablemente siguió en ejecución o fue terminado.

Decisión: Revisa los logs de error de Nginx/Apache en ese momento, luego los logs de PHP-FPM por terminación y después los logs de la aplicación para ver qué estaba haciendo.

Task 14: Inspeccionar el log de errores de Nginx por timeouts upstream

cr0x@server:~$ sudo tail -n 20 /var/log/nginx/error.log
2025/12/27 10:16:02 [error] 2281#2281: *991 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 203.0.113.10, server: example.com, request: "POST /wp-admin/admin-ajax.php HTTP/2.0", upstream: "fastcgi://unix:/run/php/php8.2-fpm.sock", host: "example.com"

Qué significa: Nginx dejó de esperar a PHP-FPM. No necesariamente que PHP murió—solo que el proxy dejó de esperar.

Decisión: Si este endpoint es legítimamente de larga ejecución, rediseñalo (async/background). Si no, encuentra el cuello de botella dentro de PHP/BD.

Task 15: Confirmar ajustes de terminación en PHP-FPM

cr0x@server:~$ sudo grep -R "request_terminate_timeout" -n /etc/php/8.2/fpm/pool.d /etc/php/8.2/fpm/php-fpm.conf
/etc/php/8.2/fpm/pool.d/www.conf:392:request_terminate_timeout = 60s

Qué significa: PHP-FPM matará peticiones a los 60s incluso si max_execution_time de PHP es mayor.

Decisión: Alinea. Si mantienes 60s en FPM, mantén PHP igual o menor. Preferible bajar y mover tareas largas fuera de banda.

Task 16: Comprobar si el opcode cache está habilitado (salud de rendimiento)

cr0x@server:~$ php -i | grep -E '^opcache.enable|^opcache.memory_consumption'
opcache.enable => On => On
opcache.memory_consumption => 128 => 128

Qué significa: Opcache está activo (bien). Si estuviera apagado, PHP recompilaría scripts constantemente, aumentando latencias y timeouts bajo carga.

Decisión: Si opcache está apagado en FPM, enciéndelo. Es uno de los pocos “ajustes de rendimiento” que realmente son aburridos y correctos.

Soluciones seguras que no generan nuevos incendios

Hay dos categorías de soluciones:

  1. Aumentar límites (timeouts, memoria) para detener errores inmediatos.
  2. Hacer el trabajo más rápido o más pequeño para que encaje dentro de límites sensatos.

Aumentar límites a veces es necesario, especialmente para importaciones o backups. Pero para cargas de página rutinarias y clics de administración, a menudo es un parche. Y los parches pertenecen a cortes, no a heridas arteriales.

Arreglo 1: Incrementar max_execution_time (solo con propósito)

Si realmente tienes una petición legítima de larga ejecución (importación grande, generación de informe pesada), puedes aumentar max_execution_time. Hazlo en el lugar correcto:

  • PHP-FPM: edita /etc/php/8.2/fpm/php.ini o sobrescribe a nivel de pool.
  • Apache mod_php: edita /etc/php/8.2/apache2/php.ini.
  • CLI WP-CLI: edita /etc/php/8.2/cli/php.ini (diferente del web).

Luego recarga servicios. No reinicies todo el servidor a menos que disfrutes de rituales.

Arreglo 2: Alinear los timeouts de la pila (proxy/web/FPM/PHP)

Los timeouts desalineados crean fallos fantasma: el cliente ve 504 mientras PHP sigue, o PHP es matado mientras Nginx sigue esperando pacientemente.

Una alineación práctica para WordPress típico:

  • Peticiones de cara al usuario: límite de 30–60 segundos (a menudo menos).
  • Acciones de administrador: 60–120 segundos si es imprescindible, pero intenta evitarlo.
  • Importaciones/exportaciones/backups: no las hagas vía web; usa WP-CLI o jobs async.

Cuando aumentes timeouts, hazlo de forma limitada. Usa bloques de ubicación para endpoints de importación si hace falta. No aumentes globalmente y luego te preguntes por qué tu pool de workers está lleno de zombies.

Arreglo 3: Mover trabajo pesado administrativo a WP-CLI

Las peticiones web son un lugar terrible para trabajo pesado. Están impulsadas por el usuario, son frágiles y tienen limitaciones de timeout. WP-CLI te da operaciones de larga ejecución sin timeouts de proxy y es más fácil de registrar.

Ejemplos: actualizaciones de plugins, search/replace en BD, importaciones grandes, reconstrucción de caché.

Arreglo 4: Reemplazar “WP-Cron por tráfico” con cron real

Esta es una de las mejores mejoras de fiabilidad que puedes hacer.

  • Desactiva WP-Cron desencadenado en cada petición.
  • Ejecuta wp cron event run --due-now desde el cron del sistema cada minuto o cada cinco minutos.

Estabiliza la ejecución de jobs y reduce picos sorpresa durante las cargas de página.

Arreglo 5: Corregir el bloat de autoload

El bloat de autoload es latencia auto-infligida. El enfoque seguro:

  1. Mide el tamaño total de autoload.
  2. Identifica los principales culpables.
  3. Para cada culpable: determina si es seguro poner autoload a ‘no’ o moverlo a transient/object cache.
  4. Prueba en staging. Despliega con rollback.

Arreglo 6: Corregir consultas lentas y añadir índices (con cuidado)

Los índices no son magia; son compensaciones. Pero la falta de índices en tablas de plugins es una causa recurrente de timeouts.

No añadas índices a lo loco en producción durante tráfico pico. Añadir un índice puede bloquear tablas o al menos aumentar I/O mientras se crea. Planifícalo.

Arreglo 7: Dimensionar correctamente los workers de PHP-FPM (ajustes pm)

Más workers no siempre significan mayor rendimiento. Demasiados workers pueden:

  • agotar memoria y provocar swap (lento, luego más lento),
  • aumentar las conexiones a la BD y la contención,
  • incrementar la contención de I/O de disco.

Parte de la realidad: núcleos de CPU, memoria, coste medio de petición. Luego fija pm.max_children a un número que puedas ejecutar sin hacer swap.

Arreglo 8: Abordar realidades de almacenamiento y sistema de ficheros

Como ingeniero de almacenamiento en la sala, aquí está la parte que la gente omite porque no es una configuración de WordPress:

  • Si la latencia de disco es alta, optimiza tu almacenamiento. Ningún ajuste de PHP arregla discos lentos.
  • Revisa condiciones de disco lleno: cuando los discos se llenan, todo se vuelve raro.
  • Los volúmenes en red (algunos discos “baratos” en la nube) pueden tener latencias impredecibles bajo límites de burst.

Broma corta #2: Poner max_execution_time a 0 es como quitar el cinturón de seguridad porque te arruga la camisa.

Tres mini-historias corporativas desde el campo

Mini-historia #1: Un incidente causado por una suposición equivocada

La empresa tenía una tienda WooCommerce “simple”. No enorme, ni diminuta. Suficiente facturación para que las notificaciones en Slack tuvieran cierto tono. El equipo de ops recibió un pico de fallos de checkout: los clientes reportaban checkouts giratorios y luego errores. Los logs de Nginx mostraban 504s.

El desarrollador de guardia asumió que era un timeout de PHP y subió max_execution_time de 30 a 120. Los errores empeoraron. No es inusual: los timeouts más largos hacen que más peticiones se acumulen. Aumenta la concurrencia, sube la presión en la BD y todo se ralentiza aún más. El sistema se convirtió en un atasco en cámara lenta.

La suposición equivocada fue pensar que el “error de timeout” significaba “PHP necesita más tiempo.” Lo que en realidad significaba era “PHP está esperando.” El processlist de MySQL mostró múltiples sesiones bloqueadas por un metadata lock. Una actualización de plugin había disparado un ALTER TABLE durante horas de negocio. El ALTER mantuvo locks el tiempo suficiente para que las consultas de checkout quedaran en cola, y los workers de PHP esperaron hasta alcanzar los timeouts del proxy.

La solución no fue “más tiempo.” Fue: detener el cambio de esquema, drenar la cola y programar los cambios de esquema fuera de hora con un enfoque de migración más seguro. Después implementaron una regla: las actualizaciones que tocan esquema ocurren en ventana de mantenimiento, validadas en staging contra una copia de datos de producción.

El postmortem fue contundente: los timeouts eran el síntoma. El bloqueo era la enfermedad. El equipo actualizó su runbook para comprobar locks de base de datos antes de tocar timeouts.

Mini-historia #2: Una optimización que salió mal

Otra organización tenía un sitio WordPress con mucho contenido y caching agresivo. Querían mejorar el rendimiento en el admin, así que ajustaron PHP-FPM para “throughput máximo”: aumentaron pm.max_children considerablemente y redujeron timeouts para “fallar rápido”. El benchmark en una caja de staging silenciosa se veía genial.

Producción discrepó. En horas pico editoriales, muchos administradores subían imágenes y editaban posts mientras el tráfico fluía. El I/O de disco se disparó porque el procesamiento de imágenes y las escrituras de caché son operaciones intensivas de escritura. Con demasiados workers de PHP-FPM, la longitud de la cola de disco aumentó, el iowait subió y cada petición se ralentizó. Empezaron a aparecer timeouts—no porque una petición individual fuera “pesada”, sino porque el sistema estaba sobresuscrito.

La optimización falló porque asumió que el cuello de botella era la CPU. No lo era. El almacenamiento era el cuello de botella, y multiplicar workers multiplicó la contención. El equipo había construido efectivamente un amplificador de concurrencia sobre un disco lento.

La solución fue contraintuitiva: reducir la concurrencia de PHP-FPM para ajustarla a la capacidad de I/O, habilitar opcache correctamente y mover el procesamiento de imágenes a una cola asíncrona. También añadieron monitorización de latencia de disco como una señal SLO de primera clase en lugar de solo CPU y memoria.

Después de eso, los timeouts desaparecieron en su mayoría. No porque permitieran más tiempo. Porque dejaron de iniciar más trabajo del que el almacenamiento podía terminar.

Mini-historia #3: Una práctica aburrida pero correcta que salvó el día

Un equipo SaaS de marketing usaba WordPress como su puerta de entrada. No eran “técnicos”, pero tenían un SRE que insistía en hábitos aburridos: retención de logs, dashboards, un entorno de staging que reflejaba producción y un “día de actualizaciones” mensual con ensayos de rollback.

Un martes, una actualización de plugin introdujo una regresión: un nuevo widget del dashboard hacía una llamada externa lenta en cada carga de página del admin. Los editores empezaron a ver errores de tiempo máximo de ejecución. Esto pudo haberse convertido en un incendio de varias horas.

Pero su práctica aburrida funcionó. El SRE comparó gráficas antes/después: la latencia HTTP saliente se disparó justo en el momento de la actualización del plugin. Los logs de PHP mostraron el timeout originándose en funciones HTTP de WordPress. Revirtieron el plugin mediante su procedimiento documentado (uno real, no “esperemos que tengamos backup”). Los errores cayeron inmediatamente.

Luego desplegaron una mitigación: deshabilitaron el widget del dashboard, añadieron un timeout externo corto y caching, y presionaron al proveedor por una solución. La historia termina con la línea que todo ops desea: “Los clientes casi no lo notaron.”

La práctica aburrida no fue magia. Fue tener observabilidad y rollback antes de necesitarlos.

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

Estos son patrones que veo repetidamente. Si reconoces uno, salta la depuración heroica y ve directo a la causa probable.

1) Síntoma: 504 Gateway Timeout en el navegador, no aparece fatal de PHP

Causa raíz: El timeout de Nginx/Apache/proxy es menor que PHP/PHP-FPM o el backend está atascado y el proxy se rinde primero.

Solución: Revisa el log de errores de Nginx por “upstream timed out.” Alinea fastcgi_read_timeout con los ajustes de terminación de PHP-FPM. Luego encuentra por qué el backend es lento (locks BD, disco, CPU) en lugar de solo subir el timeout del proxy.

2) Síntoma: “Maximum execution time of 30 seconds exceeded” durante actualizaciones de plugins

Causa raíz: El actualizador de plugins realiza escrituras en el sistema de ficheros lentamente (permisos, disco lento), llamadas remotas o descompresión bajo carga.

Solución: Ejecuta actualizaciones vía WP-CLI en tráfico bajo. Asegura la propiedad del sistema de ficheros para evitar reintentos por permisos. Verifica la latencia del almacenamiento.

3) Síntoma: Área de admin lenta, frontend “mayormente bien”

Causa raíz: Las páginas de admin disparan más consultas, cargan más plugins, llaman APIs externas o generan informes. También es más probable encontrar bloat de autoload y hooks pesados.

Solución: Perfila endpoints de admin; audita plugins que añaden widgets de dashboard; reduce opciones autoloaded; deshabilita características de admin innecesarias en producción.

4) Síntoma: Los errores se concentran durante importaciones o subidas de medios

Causa raíz: El procesamiento de imágenes (GD/Imagick) es intensivo en CPU y I/O; las importaciones hacen inserts masivos en BD y pueden provocar locks.

Solución: Usa WP-CLI para importaciones; operaciones por lotes; externaliza el procesamiento de medios o genera miniaturas de forma asíncrona. Aumenta memoria más que tiempo si estás sufriendo OOM-thrashing.

5) Síntoma: Timeouts aleatorios, especialmente con poco tráfico

Causa raíz: Acumulación de WP-Cron: las tareas no corren regularmente y luego una sola petición dispara múltiples tareas pendientes.

Solución: Desactiva WP-Cron y ejecuta cron real. Revisa jobs que se solapan y reduce la frecuencia.

6) Síntoma: Tras “aumentar rendimiento”, los timeouts empeoraron

Causa raíz: Aumentar workers de PHP-FPM o escrituras de caché amplificó la contención (BD, disco). O una “optimización” eliminó caching en endpoints de admin, incrementando la carga.

Solución: Revierte a la configuración previa conocida como buena. Dimensiona la concurrencia correctamente. Mide iowait y esperas de bloqueo en BD. Cambia una cosa a la vez.

7) Síntoma: Timeouts solo en una página/acción específica

Causa raíz: Un endpoint de un plugin realiza trabajo costoso (llamadas remotas, consulta grande, escaneo completo de tabla).

Solución: Desactiva el plugin para confirmar; luego arréglalo o sustitúyelo. Añade índices si procede. Mueve el trabajo a asincronía.

Listas de verificación / plan paso a paso

Checklist A: Estabilizar el sitio en 15 minutos (triage)

  1. Confirma el modo de fallo: fatal de PHP vs 504 vs 502. Usa logs, no intuiciones.
  2. Revisa presión de recursos: top y iostat. Si iowait es alto, no toques timeouts aún.
  3. Revisa locks BD: SHOW PROCESSLIST. Si ves metadata locks, detén cambios de esquema y elimina bloqueos.
  4. Identifica el endpoint: ¿Qué URL/acción lo dispara? Reproduce con curl -w timing.
  5. Mitiga: desactiva el plugin o función culpable si está claro; o redirige tareas pesadas a WP-CLI.

Checklist B: Hacer la solución durable (trabajo del día siguiente)

  1. Alinea timeouts entre capas: proxy/web/FPM/PHP. Documenta la configuración.
  2. Arregla bloat de autoload: mide, identifica culpables, remedia con tests.
  3. Arregla consultas lentas: habilita logging de consultas lentas temporalmente; añade índices donde se justifique.
  4. Implementa cron real: desactiva WP-Cron y programa correctamente.
  5. Dimensiona PHP-FPM: fija un pm.max_children sensato según memoria y CPU.
  6. Establece un pipeline seguro de actualizaciones: validación en staging, ventanas de mantenimiento, procedimiento de rollback.

Checklist C: Cuándo es aceptable aumentar timeouts

  • Importaciones grandes puntuales que controlas (mejor usar WP-CLI igualmente).
  • Informes solo para administradores con concurrencia limitada, donde también limitas el acceso.
  • Backups/exports ejecutados vía CLI o jobs asíncronos, no peticiones web.

Si subes timeouts para cargas de página rutinarias o checkout, probablemente estás tratando síntomas.

Datos interesantes y contexto (la historia importa)

  • El límite de tiempo de ejecución de PHP tiene décadas y fue diseñado para proteger hosting compartido de scripts descontrolados.
  • El “cron” de WordPress no es cron de sistema; es un pseudo-cron disparado por tráfico web, lo cual es ingenioso hasta que deja de serlo.
  • El clásico valor por defecto de 30 segundos es común porque muchos servidores web históricamente asumieron “un humano no esperará más” y porque previene el pinning de recursos al estilo slow-loris.
  • Las pilas de timeouts están por diseño en capas: cada capa se protege independientemente. Por eso arreglar un timeout a menudo revela el siguiente.
  • Las opciones autoloaded se cargan en cada petición, y la tabla puede convertirse en un sumidero de rendimiento cuando los plugins almacenan blobs grandes.
  • Muchas acciones de admin en WordPress son síncronas (actualizar, instalar, importar), lo que es operacionalmente problemático en tráfico de producción.
  • PHP-FPM añadió controles a nivel de pool (como timeouts de terminación) en parte para evitar que pools individuales monopolicen un servidor.
  • Los metadata locks de la base de datos se volvieron más visibles a medida que los sitios crecieron y “simplemente ejecutar ALTER TABLE” chocó con expectativas de tráfico siempre activo.
  • El object caching evolucionó como solución para cargas repetidas de opciones y consultas, pero también puede enmascarar bloat subyacente si no mides.

Preguntas frecuentes

1) ¿Debo simplemente aumentar max_execution_time a 300?

Sólo si puedes explicar qué trabajo necesita 300 segundos y por qué debe ocurrir en una petición web. Para cargas de página normales, trátalo como un bug a corregir, no como un límite a aumentar.

2) ¿Por qué ocurre solo al actualizar plugins/temas?

Las actualizaciones implican descargar, verificar, descomprimir y escribir muchos ficheros. En discos lentos o con CPU limitada puede superar 30–60 segundos. Ejecuta actualizaciones vía WP-CLI en horarios de bajo tráfico y confirma que los permisos del sistema de ficheros sean correctos.

3) Aumenté max_execution_time pero sigo obteniendo errores 504. ¿Por qué?

Porque el timeout de tu proxy/servidor web (Nginx fastcgi_read_timeout, Apache Timeout, timeout inactivo del load balancer) probablemente sea menor que el de PHP. Alinea esos valores y luego arregla la lentitud subyacente.

4) ¿Cuál es la diferencia entre max_execution_time y request_terminate_timeout?

max_execution_time lo aplica PHP. request_terminate_timeout lo aplica PHP-FPM y puede matar una petición independientemente de la configuración de PHP. Si discrepan, en la práctica gana el valor menor.

5) ¿Puede un problema de base de datos causar un error de tiempo de ejecución de PHP?

Sí. El “tiempo de ejecución” de PHP incluye el tiempo esperando la base de datos. Una consulta lenta o un bloqueo pueden consumir todo el presupuesto mientras PHP está efectivamente inactivo.

6) ¿Aumentar el límite de memoria de PHP ayuda con errores de tiempo de ejecución?

A veces. Si la petición es lenta porque hace thrashing de memoria o swap, más memoria puede reducir el tiempo de pared. Pero si es lenta por locks de BD o latencia de disco, la memoria no te salvará.

7) ¿Cómo sé si WP-Cron está involucrado?

Busca timeouts en cargas normales que se correlacionen con hooks de cron, eventos vencidos o hooks personalizados frecuentes. Usa WP-CLI para listar eventos e identificar jobs pesados. Si se acumulan eventos vencidos, pasa a cron del sistema.

8) ¿Es seguro desactivar un plugin para arreglar timeouts?

En un incidente, sí—si conoces el impacto en el negocio. Desactiva el plugin que dispara el timeout para restaurar el servicio y luego investiga. Para plugins de WooCommerce/pagos ten cuidado: desactivarlos puede romper el checkout. Prefiere flags de características o desactivación dirigida cuando sea posible.

9) ¿Por qué ocurre más en hosting compartido?

El hosting compartido suele tener límites más estrictos, vecinos ruidosos, almacenamiento más lento y menos control sobre PHP-FPM y ajustes de proxy. También es más probable que te quedes con valores por defecto bajos y con poca observabilidad.

10) ¿Cuál es la solución más “correcta SRE” para tareas largas?

Hacerlas asíncronas: encolar trabajo, devolver rápido y procesar en background con reintentos, timeouts e idempotencia. WordPress puede hacerlo de forma imperfecta, pero sigue siendo mejor que inmovilizar workers web.

Conclusión: próximos pasos que realmente deberías hacer

Si recuerdas una cosa: los errores de tiempo de ejecución suelen ser el servidor diciéndote “esta petición es demasiado grande, o el sistema es demasiado lento.” El camino seguro es demostrar cuál de los dos es.

  1. Clasifica el timeout (PHP vs proxy vs FPM) usando logs.
  2. Ejecuta el diagnóstico rápido: CPU/iowait, locks BD, reproducción del endpoint.
  3. Arregla el cuello de botella (bloat de autoload, consultas lentas, latencia de almacenamiento, FPM sobresuscrito) antes de subir límites.
  4. Mueve el trabajo pesado fuera de peticiones web: WP-CLI, cron real, jobs en background.
  5. Alinea los timeouts intencionalmente, documéntalos y mantenlos estrictos para rutas de cara al usuario.

Haz eso, y “Max execution time exceeded” volverá a ser lo que debe ser: una barandilla que rara vez golpeas, no un estilo de vida.

← Anterior
ZFS zdb -C: Leer la configuración del pool directamente desde el disco
Siguiente →
Limitación SMTP: Demuestra que es el proveedor y adáptate sin fricciones

Deja un comentario