Los 504 son el peor tipo de indisponibilidad: no es un fallo limpio, no hay una página de error agradable, solo un proxy encogiendo de hombros ante tus clientes mientras tu Slack se llena de “el sitio se está quedando colgado”. WordPress hace esto más divertido porque la falla puede estar en tres sitios a la vez: el proxy web, PHP-FPM y la base de datos, todos discutiendo sobre de quién es la culpa.
Este es un enfoque orientado a producción para demostrar si tu 504 Gateway Timeout proviene de la capa de base de datos (MySQL/MariaDB) o de la capa PHP (PHP-FPM / mod_php). No adivinanzas. No “reinicié y se fue”. Evidencia que puedes pegar en un canal de incidentes y tomar una decisión basada en hechos.
El único modelo mental que necesitas para los 504
Un 504 Gateway Timeout casi nunca es la falla real. Es el mensajero. El proxy (Nginx, Apache actuando como proxy inverso, Cloudflare, un balanceador de carga) esperó una respuesta del upstream y se quedó sin paciencia.
En un hosting típico de WordPress, la ruta de la solicitud se ve así:
- Cliente → CDN/WAF (opcional) → Nginx/Apache (proxy inverso)
- Proxy → manejador PHP (PHP-FPM o mod_php)
- PHP → base de datos (MySQL/MariaDB) y otras dependencias (Redis, APIs externas, SMTP, pasarelas de pago)
- Las respuestas regresan por el mismo camino
Así que cuando obtienes un 504, la pregunta es: ¿quién no respondió a tiempo? Ese “quién” puede ser PHP (bloqueado, lento, saturado) o la base de datos (consultas lentas, bloqueos, paradas de IO), o ambos en cadena causal (BD lenta → trabajadores PHP se acumulan → proxy se agota).
Aquí está la regla operativa que ahorra horas: el 504 es un problema de encolamiento hasta que se demuestre lo contrario. Algo se está encolando: solicitudes esperando trabajadores PHP, trabajadores PHP esperando a la BD, la BD esperando disco, o todo esperando un bloqueo.
Primera broma corta: Un 504 es como una reunión que “se quedó sin tiempo”: nadie admite que no hizo el trabajo, pero todos acuerdan reprogramarla.
Qué significa “problema de base de datos” (operativamente)
“La base de datos está lenta” no es un diagnóstico. Operativamente, significa una de las siguientes:
- Las consultas son lentas porque examinan demasiado (índices faltantes, patrones de consulta malos).
- Las consultas son lentas porque la BD está bloqueada (locks, metadata locks, transacciones largas).
- Las consultas son lentas porque la BD no puede leer/escribir lo suficientemente rápido (saturación de IO, paradas de fsync).
- Las consultas son lentas porque la BD está limitada por CPU (ordenamientos complejos, regex, joins pesados).
- Las consultas son lentas porque las conexiones están en cuello de botella (max_connections, thread pool, tormentas de conexiones).
Qué significa “problema de PHP” (operativamente)
“PHP está lento” usualmente significa una de las siguientes:
- El pool de PHP-FPM está saturado (todos los workers ocupados; solicitudes en cola).
- Los workers están bloqueados (deadlocks en el código, llamadas a APIs externas con timeouts largos, problemas de DNS).
- Los workers están muriendo / reciclando (OOM kills, max_requests muy bajo, fugas de memoria).
- Falta o mala configuración del caché de opcodes (cada petición recompila demasiado código).
- IO de archivos lento (NFS, créditos de burst de EBS, almacenamiento sobrecargado).
El truco no es debatir estas hipótesis. El truco es recopilar suficientes señales para condenar una capa.
Guía rápida de diagnóstico (verificar 1/2/3)
Esta es la secuencia de “tengo 10 minutos antes de que la dirección descubra que la página de estado también es WordPress”. No estás optimizando; estás determinando el cuello de botella y deteniendo la hemorragia.
1) Empieza en el borde: confirma que es un timeout del origen, no un berrinche del CDN
- Si Cloudflare/ALB devuelve 504, verifica si el origin es accesible y si los logs del proxy muestran timeouts de upstream.
- Si solo algunas páginas 504, sospecha de la aplicación/BD. Si todas las páginas 504 incluyendo assets estáticos, sospecha del web/proxy o la red.
2) Revisa el log de errores del proxy para detalles de timeout del upstream
- Nginx te dirá literalmente: “upstream timed out” (PHP no respondió) o “connect() failed” (PHP caído).
- Esto aún no prueba BD vs PHP; prueba que “PHP no respondió al proxy”. Entonces te preguntas: ¿PHP estaba esperando a la BD?
3) Revisa PHP-FPM: longitud de cola y saturación de max_children
- Si PHP-FPM tiene cola de listen creciendo y procesos al máximo pm.max_children, PHP está saturado (la causa puede seguir siendo BD).
- Si PHP tiene workers libres pero las solicitudes aún hacen timeout, busca llamadas bloqueadas (locks de BD, APIs externas, paradas de sistema de archivos).
4) Revisa MySQL/MariaDB: consultas activas, esperas por locks y picos en consultas lentas
- Si ves muchos threads “Waiting for table metadata lock” o consultas SELECT/UPDATE de larga duración, tienes contención en la BD.
- Si ves altos tiempos de InnoDB fsync/IO y aumento en tiempos de consulta, sospecha del almacenamiento.
5) Haz un movimiento de estabilización, no un movimiento aleatorio
- Si la BD está bloqueada: mata la transacción que bloquea, no toda la BD (a menos que disfrutes ampliar la interrupción).
- Si PHP está saturado: escala temporalmente los workers de PHP solo si la BD puede aguantarlo; de lo contrario solo te estarás auto-DDoS contra la base de datos.
- Si un endpoint de un plugin está causando el desastre: limita su tasa o desactívalo temporalmente.
Datos interesantes y breve historia (por qué los 504 se ven así)
- 504 está definido en la especificación HTTP como “Gateway Timeout”: habla explícitamente de intermediarios (proxies/gateways) que agotan su tiempo, no de la aplicación origin decidiendo expirar.
- Nginx popularizó el lenguaje “upstream” en logs y documentación, y esa terminología moldeó cómo los equipos modernos depuran: “¿qué upstream?” se volvió la primera pregunta.
- PHP-FPM se convirtió en el predeterminado en muchas pilas de WordPress porque aísla procesos PHP, ofrece controles de pool (pm.max_children) y evita el bloat de memoria del prefork de Apache en sitios concurridos.
- InnoDB sustituyó a MyISAM en la mayoría de instalaciones WordPress porque el bloqueo a nivel de fila y la recuperación ante fallos importan cuando hay escrituras concurrentes (comentarios, carritos, sesiones).
- El esquema de WordPress es intencionalmente genérico (postmeta, usermeta tablas clave/valor). Flexible, sí. También una trampa de rendimiento si consultas meta sin buenos índices.
- Las consultas lentas no siempre aparecen como páginas lentas hasta que la concurrencia aumenta. Una consulta de 1 segundo es molesta. Una consulta de 1 segundo repetida 200 veces se vuelve una denegación de servicio que pagaste.
- Los valores por defecto de timeout rara vez están alineados: timeout del CDN, timeout del proxy, fastcgi timeout, max_execution_time de PHP y net_read_timeout de MySQL pueden discrepar. La desalineación produce 504 “misteriosos”.
- Los cambios de índice pueden bloquear tablas más tiempo del que esperas, especialmente con tablas grandes y ciertos ALTER TABLE. Eso aparece como metadata locks repentinos y 504 en cascada.
- Históricamente, “simplemente añade workers” funcionó cuando las CPU eran baratas y la carga en BD ligera. A escala, esa estrategia se convierte en herbívoros en estampida auto-infligidos contra la base de datos.
Cómo se ve la “prueba”: establecer la culpa sin sensaciones
“Es la base de datos” es una afirmación. “Es PHP” es otra afirmación. La prueba es una cadena de marcas de tiempo y evidencia correlacionada que muestra dónde se pasa el tiempo.
En un informe de incidente claro, quieres al menos dos señales independientes que apunten al mismo culpable:
- Evidencia en el proxy: timeouts de upstream, tiempos de respuesta del upstream, proporción de 499/504, picos de errores.
- Evidencia en la capa PHP: estado de PHP-FPM (activos/idle, pm.max_children alcanzado), slowlog con trazas de pila, tiempo CPU de workers, longitud de cola.
- Evidencia en la BD: slow query log correlacionado con la ventana del incidente, esperas por locks, transacciones largas, esperas de IO, muchos threads en ejecución.
- Evidencia del host: steal de CPU, load average vs hilos ejecutables, iowait, latencia de disco, retransmisiones de red.
Tampoco ignores la cadena de dependencias: PHP puede ser “el que expiró”, mientras que la base de datos es “la que lo causó”. Tu trabajo es identificar el primer recurso constreñido en la cadena.
Segunda broma corta: Reiniciar servicios para arreglar un 504 es como subir el volumen de la radio para arreglar un ruido raro en el motor: cambia tu estado de ánimo, no la física.
Mentalidad de confiabilidad (una cita)
La esperanza no es una estrategia.
— frase común en la cultura de operaciones; trátala como una idea para círculos de confiabilidad.
Traducción: recopila evidencia y luego actúa.
Tareas prácticas (comandos, salidas, decisiones)
Estas son tareas reales que puedes ejecutar en un host Linux típico de WordPress. Cada una incluye: comando, qué significa la salida y qué decisión tomar a continuación. Ejecútalas en orden si estás en pánico; elige las que necesites si estás tranquilo.
Tarea 1: Confirma el 504 y mide dónde se pasa el tiempo (lado cliente)
cr0x@server:~$ curl -sS -o /dev/null -w 'code=%{http_code} ttfb=%{time_starttransfer} total=%{time_total}\n' https://example.com/
code=504 ttfb=60.001 total=60.002
Significado: Time-to-first-byte (TTFB) es básicamente igual al tiempo total, y muere alrededor de ~60s. Eso huele a timeout del proxy, no a una página de error generada por la app.
Decisión: Ve a los logs del proxy para encontrar el upstream que expiró y el valor de timeout configurado.
Tarea 2: Revisa el log de errores de Nginx por “upstream timed out” (evidencia de proxy)
cr0x@server:~$ sudo tail -n 30 /var/log/nginx/error.log
2025/12/26 11:18:41 [error] 1842#1842: *991 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 203.0.113.10, server: example.com, request: "GET / HTTP/2.0", upstream: "fastcgi://unix:/run/php/php8.2-fpm.sock", host: "example.com"
Significado: Nginx esperó por PHP-FPM y no recibió cabeceras de respuesta a tiempo. Esto prueba que el proxy no es la parte lenta; está esperando.
Decisión: Inspecciona la saturación de PHP-FPM y las solicitudes lentas. Aún no es suficiente para culpar a la BD; PHP puede estar esperando a la BD.
Tarea 3: Confirma la configuración de timeout fastcgi de Nginx (evita perseguir fantasmas)
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -E 'fastcgi_read_timeout|proxy_read_timeout|send_timeout' | head
fastcgi_read_timeout 60s;
send_timeout 60s;
Significado: El corte de 60 segundos coincide con tu timing de curl. Bien. Estás depurando la ventana correcta, no algún timeout del cliente.
Decisión: No aumentes los timeouts aún. Primero averigua qué está tardando >60 segundos. Aumentar timeouts oculta fallas y hace las colas más grandes.
Tarea 4: Revisa el estado del pool de PHP-FPM (¿te quedaste sin workers?)
cr0x@server:~$ sudo ss -lxp | grep php-fpm
u_str LISTEN 0 128 /run/php/php8.2-fpm.sock 44123 * 0 users:(("php-fpm8.2",pid=1640,fd=9))
Significado: PHP-FPM está escuchando. Si esta línea falta, PHP-FPM está caído o la ruta del socket difiere.
Decisión: Si falta: restaura el servicio PHP-FPM. Si está presente: verifica si está saturado y encolando.
Tarea 5: Inspecciona pm.max_children de PHP-FPM (el cuello de botella clásico)
cr0x@server:~$ sudo grep -R "pm.max_children" /etc/php/8.2/fpm/pool.d/*.conf
/etc/php/8.2/fpm/pool.d/www.conf:pm.max_children = 20
Significado: Tienes como máximo 20 solicitudes PHP concurrentes en este pool. Eso puede estar bien o ser peligrosamente bajo, según el tiempo por solicitud y el tráfico.
Decisión: A continuación, verifica si esos 20 están todos ocupados y si las solicitudes están en cola.
Tarea 6: Lee los logs de PHP-FPM por “server reached pm.max_children”
cr0x@server:~$ sudo tail -n 30 /var/log/php8.2-fpm.log
[26-Dec-2025 11:18:12] WARNING: [pool www] server reached pm.max_children setting (20), consider raising it
[26-Dec-2025 11:18:13] WARNING: [pool www] server reached pm.max_children setting (20), consider raising it
Significado: PHP-FPM está saturado. Las solicitudes están encolándose. Esta es evidencia contundente de que la capacidad de PHP es un límite.
Decisión: Determina si PHP está lento por su propio trabajo en CPU o si está esperando BD/IO. Aumentar max_children a ciegas puede destrozar la BD.
Tarea 7: Comprueba el recuento actual de procesos PHP-FPM y uso de CPU
cr0x@server:~$ ps -o pid,pcpu,pmem,etime,cmd -C php-fpm8.2 --sort=-pcpu | head
PID %CPU %MEM ELAPSED CMD
1721 62.5 2.1 01:12 php-fpm: pool www
1709 55.2 2.0 01:11 php-fpm: pool www
1698 48.9 1.9 01:10 php-fpm: pool www
Significado: Si los workers muestran alta CPU, PHP puede estar realizando trabajo pesado (o atascado en bucles intensos). Si muestran baja CPU pero tiempo de ejecución largo, probablemente estén esperando IO (BD, disco, red).
Decisión: Si CPU alta: perfila/optimiza PHP o reduce trabajo (plugins, caching). Si CPU baja pero tiempo alto: revisa BD e IO a continuación.
Tarea 8: Habilita o lee el slowlog de PHP-FPM para capturar trazas de solicitudes lentas
cr0x@server:~$ sudo grep -R "slowlog\|request_slowlog_timeout" /etc/php/8.2/fpm/pool.d/www.conf
request_slowlog_timeout = 10s
slowlog = /var/log/php8.2-fpm.slow.log
cr0x@server:~$ sudo tail -n 20 /var/log/php8.2-fpm.slow.log
[26-Dec-2025 11:18:39] [pool www] pid 1721
script_filename = /var/www/html/index.php
[0x00007f2f0c...] mysqli_query() /var/www/html/wp-includes/wp-db.php:2056
[0x00007f2f0c...] query() /var/www/html/wp-includes/wp-db.php:1945
[0x00007f2f0c...] get_results() /var/www/html/wp-includes/wp-db.php:2932
Significado: Esto es la prueba contundente cuando aparece: PHP es lento porque está dentro de una llamada a la base de datos. Si ves curl_exec(), file_get_contents() o funciones de DNS en su lugar, el culpable está en otro sitio.
Decisión: Si el slowlog muestra llamadas a BD: ve a diagnósticos de MySQL inmediatamente. Si muestra llamadas HTTP externas: aisla ese plugin/servicio y añade timeouts/circuit breakers.
Tarea 9: Revisa los estados de hilos de MySQL (bloqueos y ejecuciones largas)
cr0x@server:~$ sudo mysql -e "SHOW FULL PROCESSLIST\G" | egrep -A2 "State:|Time:|Info:" | head -n 40
Time: 58
State: Waiting for table metadata lock
Info: ALTER TABLE wp_postmeta ADD INDEX meta_key (meta_key)
Time: 55
State: Sending data
Info: SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts LEFT JOIN wp_postmeta ...
Significado: “Waiting for table metadata lock” es una bandera roja: un cambio de esquema o DDL largo está bloqueando lecturas/escrituras. “Sending data” por mucho tiempo sugiere escaneos grandes o IO lento.
Decisión: Si metadata lock: encuentra y mata la transacción que bloquea o prográmalo adecuadamente. Si escaneos largos: revisa slow query log, índices y presión del buffer pool.
Tarea 10: Revisa el estado de InnoDB por esperas de locks y paradas de IO
cr0x@server:~$ sudo mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
=====================================
2025-12-26 11:18:45 0x7f0a4c2
TRANSACTIONS
------------
Trx id counter 12904421
Purge done for trx's n:o < 12904400 undo n:o < 0 state: running
History list length 1987
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 12904388, ACTIVE 62 sec
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 418, OS thread handle 139682..., query id 9812 10.0.0.15 wpuser updating
UPDATE wp_options SET option_value='...' WHERE option_name='woocommerce_sessions'
Significado: Transacciones activas largas y una lista de historial creciente indican retraso en purge y posible contención. Las actualizaciones a tablas calientes (options, sessions) a menudo causan acumulación.
Decisión: Si ves una transacción larga bloqueando a muchas: identifícala y considera matarla (con cuidado). Luego corrige el comportamiento de la app que mantiene locks demasiado tiempo.
Tarea 11: Habilita e inspecciona el slow query log de MySQL (prueba correlacionada por tiempo)
cr0x@server:~$ sudo mysql -e "SHOW VARIABLES LIKE 'slow_query_log%'; SHOW VARIABLES LIKE 'long_query_time';"
+---------------------+------------------------------+
| Variable_name | Value |
+---------------------+------------------------------+
| slow_query_log | ON |
| slow_query_log_file | /var/log/mysql/mysql-slow.log|
+---------------------+------------------------------+
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| long_query_time | 1.000 |
+-----------------+-------+
cr0x@server:~$ sudo tail -n 25 /var/log/mysql/mysql-slow.log
# Time: 2025-12-26T11:18:21.123456Z
# Query_time: 12.302 Lock_time: 0.000 Rows_sent: 10 Rows_examined: 2450381
SELECT * FROM wp_postmeta WHERE meta_key = '_price' ORDER BY meta_value+0 DESC LIMIT 10;
Significado: Rows_examined en millones para una consulta sencilla es el tipo de cosa que convierte tráfico en timeouts. Esto es culpabilidad sólida de BD con marcas de tiempo.
Decisión: Añade/ajusta índices, reescribe consultas (a menudo generadas por plugins) o introduce caching/búsqueda. También investiga por qué esa consulta se disparó ahora (nuevo plugin, funcionalidad, campaña).
Tarea 12: Observa en tiempo real hilos y consultas de MySQL (¿se está ahogando la BD?)
cr0x@server:~$ sudo mysqladmin extended-status -ri 2 | egrep "Threads_running|Questions|Slow_queries"
Threads_running 34
Questions 188420
Slow_queries 912
Threads_running 37
Questions 191102
Slow_queries 925
Significado: Threads_running en aumento bajo carga significa que la concurrencia se está acumulando dentro de MySQL. Si Threads_running se mantiene bajo pero PHP hace timeout, la BD podría no ser el cuello de botella.
Decisión: Si Threads_running es alto: reduce el coste de las consultas y la contención; considera réplicas de lectura para endpoints de solo lectura. Si es bajo: vuelve a enfocarte en PHP/llamadas externas/almacenamiento.
Tarea 13: Revisa iowait y latencia de disco a nivel host (el almacenamiento suele ser el villano silencioso)
cr0x@server:~$ iostat -xz 1 3
avg-cpu: %user %nice %system %iowait %steal %idle
12.34 0.00 4.12 28.90 0.00 54.64
Device r/s w/s rKB/s wKB/s await svctm %util
nvme0n1 120.0 210.0 6400.0 8200.0 48.20 1.10 98.00
Significado: iowait cerca del 30% y %util del disco cercano al 100% con await alto significa que el almacenamiento está saturado. MySQL se volverá lento incluso si la CPU parece bien.
Decisión: Si el almacenamiento es el cuello de botella: corrige el IO (disco más rápido, mejores IOPS, reducir amplificación de escritura, afinar InnoDB, mover tmpdir, reducir volumen de logs). No solo añadas workers PHP.
Tarea 14: Busca OOM kills o presión del kernel (espiral silenciosa de muerte de PHP)
cr0x@server:~$ sudo journalctl -k -n 50 | egrep -i "oom|killed process" | tail
Dec 26 11:17:59 server kernel: Out of memory: Killed process 1721 (php-fpm8.2) total-vm:1234567kB, anon-rss:456789kB, file-rss:0kB, shmem-rss:0kB
Significado: Si los workers PHP están siendo OOM-killados, el proxy ve timeouts y resets. Puede parecer “504s aleatorios”.
Decisión: Reduce el uso de memoria de PHP (plugins pesados), limita la memoria por proceso, ajusta el dimensionado del pool, añade RAM y asegúrate de que el swap no sea un desastre de rendimiento.
Tarea 15: Confirma que el log de depuración de WordPress no empeora las cosas
cr0x@server:~$ grep -n "WP_DEBUG" /var/www/html/wp-config.php
90:define('WP_DEBUG', false);
91:define('WP_DEBUG_LOG', false);
Significado: Dejar WP_DEBUG_LOG habilitado en un sitio con tráfico puede crear escrituras intensas en disco, convirtiendo un problema menor en contención de IO.
Decisión: Mantén el debug logging desactivado en producción por defecto; actívalo temporalmente con ventanas controladas y rotación de logs cuando sea necesario.
Tarea 16: Prueba si PHP está esperando a la BD usando strace (quirúrgico, no para novatos)
cr0x@server:~$ sudo strace -p 1721 -tt -T -e trace=network,read,write,poll,select -s 80
11:18:40.101203 poll([{fd=12, events=POLLIN}], 1, 60000) = 0 (Timeout) <60.000312>
Significado: Un worker PHP bloqueado en poll/select durante 60 segundos está esperando IO de red—a menudo el socket de la base de datos o un servicio HTTP externo.
Decisión: Si es el socket de BD, enfócate en MySQL. Si es una IP externa, arregla esa integración (timeouts, reintentos, circuit breaking, caching).
Ahora tienes suficientes herramientas para demostrar dónde se pasa el tiempo. Siguiente: reconocimiento de patrones, porque las señales se agrupan de formas predecibles.
Base de datos vs PHP: patrones de señales que los separan
Patrón A: pm.max_children de PHP-FPM alcanzado + slowlog PHP muestra llamadas mysqli
Culpable más probable: latencia o locks en la base de datos que causan acumulación de workers PHP.
Cómo se ve:
- Nginx: “upstream timed out while reading response header from upstream”
- Log de PHP-FPM: “server reached pm.max_children”
- Slowlog de PHP: trazas de pila en wp-db.php / mysqli_query()
- MySQL: Threads_running elevado; picos en slow query log; processlist muestra consultas largas o esperas por locks
Haz esto: trata la BD como causa raíz. Reduce la carga en la BD primero, luego ajusta la concurrencia de PHP.
Patrón B: pm.max_children alcanzado + workers PHP con alta CPU + BD tranquila
Culpable más probable: trabajo a nivel PHP (bucles en plantillas, lógica de plugins costosa, procesamiento de imágenes, misses de caché, cache de objetos pobre).
Cómo se ve:
- Workers PHP muestran %CPU alto y tiempos de ejecución largos
- MySQL Threads_running moderado, slow query log sin picos
- Las solicitudes que hacen timeout suelen ser endpoints específicos (search, admin-ajax, filtros de producto)
Haz esto: aisla el endpoint, añade caching, habilita OPcache correctamente, perfila con muestreo (no tracing masivo durante un incidente).
Patrón C: PHP tiene workers inactivos, pero las solicitudes siguen 504
Culpable más probable: desajuste de configuración del proxy, backlog del socket PHP, conectividad con el upstream o algo fuera de PHP/BD (DNS, API externa, sistema de archivos).
Cómo se ve:
- Errores de Nginx pueden mostrar connect() failed, recv() failed, o resets intermitentes del upstream
- Logs de PHP-FPM podrían mostrar child exited, segfault, o nada en absoluto
- Logs del host podrían mostrar OOM kills, paradas de disco o problemas de red
Haz esto: valida sockets, backlogs, límites del kernel y dependencias externas; no te aferres únicamente a MySQL.
Patrón D: Threads_running de BD alto + iowait alto + await de disco alto
Culpable más probable: el almacenamiento limita la BD, que limita a PHP, lo cual provoca 504.
Haz esto: arregla el IO. A veces el “problema de la BD” es “compramos el disco más barato”.
Patrón E: Esperas por locks súbitas, especialmente metadata locks
Culpable más probable: DDL durante peak, migraciones de plugins o un “cambio rápido de índice” realizado en producción sin considerar comportamiento de bloqueo.
Haz esto: detén el DDL, reprograma con cambios de esquema online y aplica guardrails.
Tres microhistorias corporativas desde las trincheras
Microhistoria 1: El incidente provocado por una suposición errónea
Tenían un sitio WordPress de marketing y una tienda WooCommerce, alojados en una VM “bastante potente”. Una oleada de 504s llegó durante el lanzamiento de una campaña. El ingeniero on-call revisó el log de Nginx y vio timeouts upstream a PHP-FPM. Asumió, con confianza, que PHP-FPM necesitaba más workers.
Aumentaron pm.max_children. Los 504s empeoraron. La CPU de la base de datos subió, luego la latencia del disco se disparó, y el sitio entero empezó a fallar de maneras inconsistentes. Ya no era solo el checkout: las páginas principales también hacían timeout.
El verdadero culpable fue un patrón de consulta único introducido por un widget “filtrar productos por precio”. Usaba postmeta de modo que escaneaba rangos enormes, y se ejecutaba en cada vista de categoría. La base de datos no estaba sana, pero sobrevivía cuando la concurrencia estaba limitada. Aumentar workers PHP incrementó el número de consultas costosas concurrentes. La BD alcanzó la saturación de IO. Todo se encoló en la pila.
Se estabilizaron revirtiendo el widget, limpiando cachés y devolviendo la concurrencia de PHP a niveles sensatos. El postmortem no fue “no escales PHP”. Fue sobre no asumir “timeout PHP = problema PHP”. PHP fue la víctima. La base de datos fue la escena del crimen.
Microhistoria 2: La optimización que retrocedió
Un equipo intentó ser listo: movieron las subidas de WordPress y partes del código a un filesystem en red para “simplificar despliegues” entre dos nodos web. Funcionó en pruebas. Luego el tráfico de producción llegó y comenzaron los 504s: esporádicos al principio, luego correlacionados con picos (campañas de email, features en la página principal).
Todo parecía normal: MySQL Threads_running no era una locura, la CPU no estaba al máximo, PHP-FPM tenía capacidad. Pero las solicitudes seguían colgando lo suficiente como para que Nginx se rindiera. Alguien insistió en que “definitivamente era la base de datos” porque WordPress siempre es la base de datos. Esa afirmación duró medio día.
El punto de inflexión fue capturar un slowlog de PHP-FPM: las trazas estaban en operaciones de archivo y rutas de autoload, no en mysqli. Al mismo tiempo, métricas del host mostraron picos en iowait. El filesystem en red tenía latencias periódicas y retransmisiones ocasionales. Los workers PHP estaban inactivos desde la perspectiva de CPU, bloqueados en lecturas de archivo.
La “optimización” (almacenamiento compartido) redujo la fricción de despliegue pero añadió una dependencia de latencia en cada solicitud. La solución fue aburrida: sistema de archivos local para el código, almacenamiento de objetos para uploads con caching agresivo, y un mecanismo de despliegue que no dependiera de POSIX compartido. El rendimiento volvió instantáneamente, y la base de datos—sorprendentemente—estaba bien.
Microhistoria 3: La práctica aburrida pero correcta que salvó el día
Otra compañía ejecutaba WordPress como parte de una plataforma más grande. No eran perfectos, pero tenían un hábito que parecía casi anticuado: cada capa tenía un conjunto estándar de dashboards y logs, y mantenían los timeouts alineados. Timeout del proxy, fastcgi timeout, max_execution_time de PHP, timeouts de BD. Todo documentado. Todo consistente.
Una tarde vieron un aumento de 504s. El primer respondedor revisó logs de Nginx: upstream timeouts. Luego revisó PHP-FPM: pm.max_children no estaba alcanzado, pero el slowlog mostró llamadas wp-db. Fueron inmediatamente a MySQL y vieron esperas por locks alrededor de un plugin de migración que había iniciado un ALTER TABLE en una tabla de alto tráfico.
Puesto que tenían una práctica aburrida—slow query log habilitado con umbrales sensatos y un calendario de cambios—supieron exactamente qué cambió y cuándo. Detuvieron la migración, la reprogramaron fuera de pico con herramientas más seguras y los 504s desaparecieron. Sin reinicios aleatorios, sin “escalemos todo”, sin desfile de culpas de una semana.
Su mayor ventaja no fue una herramienta de observabilidad elegante. Fue la consistencia: timeouts alineados y señales baseline siempre activas. En respuesta a incidentes, lo aburrido es una característica.
Errores comunes: síntoma → causa raíz → solución
Estos son los patrones que consumen más tiempo porque suenan intuitivos y están equivocados en producción.
1) Síntoma: “Nginx muestra upstream timed out, así que PHP está roto.”
Causa raíz: PHP está bien; está esperando locks o consultas lentas en MySQL.
Solución: Usa el slowlog de PHP-FPM para confirmar dónde está atascado. Si está en wp-db.php, ve directo a processlist de MySQL y al estado de InnoDB; resuelve locks y coste de consultas.
2) Síntoma: “Aumentemos fastcgi_read_timeout para que los clientes dejen de ver 504.”
Causa raíz: Estás enmascarando latencia; las colas crecen; eventualmente todo colapsa y obtienes timeouts igualmente, solo más lentos.
Solución: Mantén los timeouts lo suficientemente estrictos para que la falla afloren rápido. Reduce la latencia de cola arreglando el cuello de botella y añadiendo caching/limitación de tasa donde corresponda.
3) Síntoma: “Aumentar pm.max_children empeoró las cosas.”
Causa raíz: La base de datos era el recurso limitante; más concurrencia PHP aumentó la contención y el IO en la BD.
Solución: Trata la concurrencia de PHP como un generador de carga. Ajústala al nivel que la BD pueda servir. Reduce consultas costosas, añade índices y cachea lecturas calientes.
4) Síntoma: “Solo wp-admin hace 504s, el frontend está bien.”
Causa raíz: Las páginas de administración suelen ejecutar consultas más pesadas (listados de posts con filtros), checks de plugins y comportamientos tipo cron.
Solución: Captura slowlog para endpoints de admin. Revisa admin-ajax por bucles calientes y llamadas de plugins. Añade cache de objetos y audita plugins.
5) Síntoma: “Los 504s ocurren en ráfagas después de picos de tráfico.”
Causa raíz: Colapso por encolamiento: misses de caché, estampidas, o tormentas de conexiones a la BD.
Solución: Implementa caching con protección contra estampidas, asegura conexiones persistentes a BD sensatas y limita endpoints abusivos (xmlrpc, wp-login, admin-ajax).
6) Síntoma: “La CPU de la BD es baja, así que no puede ser el problema.”
Causa raíz: La BD está limitada por IO o por locks, no por CPU.
Solución: Mira iowait, await de disco, tasa de aciertos del buffer pool y esperas por locks. La CPU es solo una forma de estar jodido.
7) Síntoma: “El slow query log está vacío, así que las consultas no son lentas.”
Causa raíz: slow_query_log está apagado, long_query_time demasiado alto, o el problema son locks (Lock_time puede ser grande mientras Query_time parece modesto según la configuración de logging).
Solución: Habilita slow query log con un umbral realista (a menudo 0.5–1s para sitios ocupados). Correlaciona con esperas por locks y duración de transacciones.
8) Síntoma: “Se va después de reiniciar, así que era una fuga de memoria.”
Causa raíz: Reiniciar drena colas y limpia locks; no arreglaste el disparador (patrón de tráfico, regresión de consultas, DDL, stall de API externa).
Solución: Trata los reinicios como mitigación temporal. Captura evidencia antes de reiniciar: slowlog, processlist, iostat, logs de error.
Listas de verificación / plan paso a paso
Paso a paso: demostrar BD vs PHP en menos de 30 minutos
- Obtén una muestra con marca de tiempo: ejecuta curl con tiempos para confirmar duración del timeout y frecuencia.
- Revisa el log del proxy: confirma upstream timed out e identifica el upstream (socket php-fpm, host upstream).
- Revisa saturación de PHP-FPM: busca advertencias de pm.max_children y cuenta workers ocupados.
- Revisa slowlog de PHP: confirma si las solicitudes lentas están en llamadas mysqli o en otro sitio.
- Revisa processlist de la BD: busca esperas por locks, ejecuciones largas y consultas repetidas costosas.
- Revisa estado de InnoDB: identifica transacciones largas y locks bloqueantes.
- Revisa slow query log: correlaciona picos en Query_time/Rows_examined con la ventana del incidente.
- Revisa salud del almacenamiento/host: iowait, latencia de disco, OOM kills, steal de CPU.
- Haz un cambio de estabilización: mata bloqueador, desactiva endpoint causante, limita tasa, o escala temporalmente la capa correcta.
- Registra lo que viste: pega las líneas clave de logs y salidas de comandos en la línea de tiempo del incidente.
Lista de estabilización (qué hacer durante el incidente)
- Desactiva el endpoint más problemático si es posible (handlers admin-ajax, búsquedas/filtrado pesados).
- Reduce la concurrencia en el generador de carga si la BD se está fundiendo (limita PHP-FPM o añade rate limiting en Nginx).
- Detén inmediatamente cambios de esquema si están bloqueando tablas calientes.
- Si IO está saturado, detén jobs en background que chafan disco (backups, indexación, logs de debug, sincronización de archivos).
- Prefiere kills dirigidos (transacción que bloquea) en lugar de reiniciar MySQL a ciegas.
Lista de endurecimiento (qué hacer después del incidente)
- Mantén configurado y probado el slowlog de PHP-FPM (no tiene que estar siempre ruidoso, pero debe estar listo).
- Mantén el slow query logging disponible (habilitado o fácil de habilitar) y sabe dónde vive el log y cómo rota.
- Alinea timeouts para que una solicitud no muera misteriosamente en diferentes capas con relojes distintos.
- Trata la concurrencia de PHP como una palanca con consecuencias: aumentar max_children incrementa carga en la BD. Confirma la capacidad de la BD antes.
- Mide la latencia del almacenamiento cuando “la base de datos está lenta”. IO es el eje oculto que la mayoría de pilas WordPress ignoran hasta que arde.
- Después del incidente, elimina el disparador: corrige la consulta, indexa adecuadamente, desactiva el comportamiento del plugin, cachea el camino caro o rediseña el endpoint.
Preguntas frecuentes
1) Si Nginx dice “upstream timed out”, ¿significa que PHP es el problema?
No. Significa que Nginx no recibió respuesta del upstream (a menudo PHP-FPM) a tiempo. PHP puede estar esperando MySQL, disco o una API externa. Usa el slowlog de PHP-FPM para ver dónde está atascado el código.
2) ¿Cuál es la forma más rápida de demostrar que la base de datos causa 504s?
Correlaciona tres cosas en la misma ventana temporal: trazas del slowlog de PHP en funciones mysqli, processlist de MySQL mostrando consultas largas/esperas por locks, y picos en el slow query log.
3) ¿Cuál es la forma más rápida de demostrar que la capacidad de PHP-FPM es el cuello de botella?
Encuentra advertencias “server reached pm.max_children” más una cola/backlog creciente, y confirma que MySQL no está sobrecargado (Threads_running estable, sin tormentas de locks). Si PHP consume CPU al 100%, está haciendo demasiado trabajo por solicitud.
4) ¿Debería aumentar fastcgi_read_timeout para evitar 504s?
Sólo como medida de contención temporal y sólo si entiendes el impacto de encolamiento. Timeouts largos pueden convertir latencia intermitente en saturación sostenida. Arregla la cola larga, no la ocultes.
5) ¿Cómo diferencio un problema de locks de una consulta lenta en MySQL?
Los locks aparecen como estados “Waiting for … lock” en processlist y en secciones de lock wait en InnoDB status. Las consultas lentas muestran alto Query_time y Rows_examined en slow query logs, frecuentemente con estados “Sending data”.
6) ¿Por qué los 504s ocurren más en checkout o carrito de WooCommerce?
El checkout toca tablas con muchas escrituras (sessions, orders, order meta) y puede disparar llamadas externas (pasarelas de pago, APIs de impuestos/envío). Esa combinación lo hace sensible tanto a contención de BD como a latencia externa.
7) ¿Puede Redis/caching de objetos arreglar los 504s?
Pueden ayudar si tu cuello de botella son consultas de lectura repetidas (options, postmeta) y tienes buenas tasas de cache hit. No solucionarán contención por escrituras ni un cambio de esquema que bloquee todo.
8) ¿Por qué veo 504s pero la CPU de MySQL está baja?
Porque la BD puede estar limitada por IO (alto await de disco), por locks (esperas) o por red. CPU baja no implica salud. Mira iowait, latencia de disco y esperas por locks.
9) ¿Es seguro matar una consulta MySQL durante un incidente?
A veces es la decisión correcta—especialmente si una transacción bloquea a muchas otras. Pero sé deliberado: identifica el bloqueador, entiende si es una escritura crítica y espera errores en la aplicación para solicitudes que usen esa transacción.
10) ¿Y si ni la BD ni PHP parecen obviamente malos?
Entonces sospecha de dependencias: latencia DNS, llamadas HTTP externas, stalls en filesystem de red, OOM kills del kernel o mala configuración del proxy. El slowlog de PHP es tu brújula: apunta a la función donde desaparece el tiempo.
Conclusión: pasos siguientes que realmente reducen los 504
Si tomas una lección operativa de esto: no debatas si es “base de datos o PHP” en abstracto. Demuestra dónde se pasa el tiempo con evidencia con marcas de tiempo desde el proxy, PHP-FPM y MySQL. Estás construyendo una cadena causal, no una corazonada.
Pasos prácticos siguientes:
- Mantén configurado el slowlog de PHP-FPM (con un umbral sensato como 5–10 segundos) para capturar trazas de pila durante incidentes reales.
- Mantén usable el registro de consultas lentas (habilitado o rápidamente habilitable), y sabe dónde está el log y cómo rota.
- Alinea timeouts para que una solicitud no muera misteriosamente en distintas capas con relojes distintos.
- Trata la concurrencia de PHP como una palanca con consecuencias: aumentar max_children incrementa la carga en la base de datos. Confirma la capacidad de la BD primero.
- Mide la latencia del almacenamiento cuando “la base de datos está lenta”. IO es el eje oculto que la mayoría de pilas WordPress ignoran hasta que hay fuego.
- Después del incidente, elimina el disparador: arregla la consulta, indexa correctamente, desactiva el comportamiento del plugin, cachea el camino costoso o rediseña el endpoint.
Los 504 no son misteriosos. Simplemente son tu infraestructura diciéndote, con educación, que una parte de tu pila dejó de seguir el ritmo. La educación se acaba cuando la ignoras.