Solucionar nginx 504 Gateway Time-out: encontrar la causa en vez de subir el plazo

Publicado el 21 min de lectura

En un 504 el backend estaba accesible, solo respondió demasiado despacio. Cómo averiguar con el log de tiempos dónde se consume el tiempo, cuál de los muchos plazos se aplica de verdad y por qué un plazo más alto casi siempre se limita a aplazar la caída.

Un 504 Gateway Time-out es la más paciente de todas las páginas de error. nginx aceptó la petición, la pasó al backend y luego esperó hasta que venció un plazo fijado internamente. El backend estuvo accesible todo el tiempo, solo que no respondió a tiempo. Ahí está justamente la diferencia con el código vecino: en el 502 Bad Gateway el backend responde mal o no responde en absoluto, mientras que en el 504 responde demasiado despacio. Esta guía te enseña cómo medir dónde se consume realmente el tiempo, cuál de los muchos plazos es el que acaba aplicándose y por qué subir ese límite es casi siempre la peor de las respuestas disponibles.

Todos los datos se refieren a Debian 13 (trixie), Debian 12 (bookworm), Ubuntu 24.04 LTS y Ubuntu 22.04 LTS. Los comandos están escritos para trabajar como root; si trabajas como usuario normal, antepón sudo. En los ejemplos aparece PHP 8.4, así que sustituye el número de versión por el de tu sistema:

SistemaPHPServicioConfiguración
Debian 13 (trixie)8.4php8.4-fpm/etc/php/8.4/fpm/
Debian 12 (bookworm)8.2php8.2-fpm/etc/php/8.2/fpm/
Ubuntu 24.04 LTS8.3php8.3-fpm/etc/php/8.3/fpm/
Ubuntu 22.04 LTS8.1php8.1-fpm/etc/php/8.1/fpm/
ls /etc/php/

Las directivas de nginx se llaman igual en los cuatro sistemas. Las diferencias están del lado de PHP y de la base de datos, y quedan señaladas en los puntos correspondientes.

Quién se ha rendido decide en qué dirección buscar

Antes de abrir ningún archivo, responde a una pregunta: ¿qué capa ha cortado? El código de estado ya lo delata.

CódigoQué ha pasadoDónde buscar
500 Internal Server ErrorEl backend respondió, pero la respuesta fue un errorEl log de la aplicación
502 Bad GatewayLa conexión no llegó a establecerse o se cortóServicio, socket, permisos, caídas
504 Gateway Time-outLa conexión estaba en pie, la respuesta no llegó dentro del plazoEl tiempo de ejecución en el backend
408 Request TimeoutEl visitante no terminó de enviar a tiempo su propia peticiónSubidas de archivos, conexiones lentas
499 (solo en el log)El visitante canceló antes de que nginx terminaraDemasiado lento, pero por debajo del plazo

La fila del 499 es la más infravalorada. No es un error, sino un sistema de alerta temprana: el visitante cerró la pestaña porque la página tardaba demasiado. Si un 504 desaparece después de subir el plazo y en su lugar empiezan a salir 499, no se ha resuelto nada.

awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head

El campo 9 corresponde al formato estándar combined. Muchos 504 con pocos 499 apuntan a páginas pesadas concretas; la imagen contraria, a una aplicación lenta de forma generalizada.

Antes de cambiar nada: el camino de vuelta

El diagnóstico de los dos apartados siguientes es de solo lectura. El riesgo empieza donde tocas configuraciones, y eso puede parar el servicio por tres vías: una configuración de nginx defectuosa impide que arranque el servidor web, un archivo de pool defectuoso impide que arranque PHP-FPM, y unos límites subidos con demasiada generosidad pueden agotar la RAM. El último caso es el más desagradable, porque entonces el kernel mata procesos y no tiene por qué tocarle al culpable. Si le toca al servicio SSH, el servidor deja de poder manejarse por red.

Por eso, haz primero copias, por debajo de /root y nunca en el directorio web. La marca de tiempo en el nombre es importante, porque rara vez se interviene una sola vez y cp -a sobrescribe sin decir nada una copia que ya exista:

mkdir -p /root/backups
cp -a /etc/nginx/nginx.conf /root/backups/nginx.conf.$(date +%F-%H%M)
cp -a /etc/nginx/sites-available/example.com /root/backups/example.com.$(date +%F-%H%M)
cp -a /etc/php/8.4/fpm/php.ini /root/backups/php.ini.$(date +%F-%H%M)
cp -a /etc/php/8.4/fpm/pool.d/www.conf /root/backups/www.conf.$(date +%F-%H%M)

El camino de vuelta son tres líneas, y el orden es deliberado:

cp -a /root/backups/example.com.2026-09-03-1030 /etc/nginx/sites-available/example.com
nginx -t
systemctl reload nginx

Usa reload en lugar de restart mientras puedas. reload solo adopta la nueva configuración si está libre de errores. Un restart termina primero el proceso en marcha y, si hay un fallo, te deja sin servidor web.

Si el servidor deja de responder por completo, en los servidores root KVM y en los servidores dedicados de KernelHost tienes la consola VNC en el área de cliente. Cuelga de la capa de virtualización o de la propia conexión, no del stack de red del sistema invitado, así que sigue funcionando aunque ya no haya ningún servicio accesible. Entra ahí una vez antes de empezar y asegúrate de conocer la contraseña de root. Comando de comprobación después de cada intervención:

systemctl is-active nginx php8.4-fpm
free -m

La línea del log de errores que decide el caso

Un 504 siempre deja rastro:

2026/09/03 10:12:33 [error] 812#812: *5 upstream timed out (110: Connection timed out)
while reading response header from upstream, client: 203.0.113.7, server: example.com,
request: "GET /report.php HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.4-fpm.sock:"

Lo decisivo no es el número de error 110, que es el mismo en todos los 504, sino la fase que viene detrás:

  • while connecting to upstream: el establecimiento de la conexión no llegó a producirse. Con un backend remoto es casi siempre un filtro de paquetes que descarta los paquetes en vez de rechazarlos, porque un rechazo volvería de inmediato y daría un 502.
  • while sending request to upstream: nginx no consiguió soltar el cuerpo de la petición, algo típico en subidas grandes.
  • while reading response header from upstream: el caso normal. El backend lo ha recibido todo y sigue calculando sin enviar siquiera la primera línea de cabecera.
  • while reading upstream: las cabeceras llegaron y después se atascó el cuerpo. Esto se ve en respuestas por streaming y en exportaciones.
grep -n "upstream timed out" /var/log/nginx/error.log | tail -20

Muchos hosts virtuales escriben en un log de errores propio. Dónde está ese archivo y por qué al buscar en sites-enabled hace falta la -R mayúscula lo explica el artículo sobre el 502. Si la búsqueda se queda vacía aunque el navegador muestre un 504, el error no viene de este nginx.

Dónde se consume el tiempo: medir en vez de adivinar

nginx puede registrar en cada petición cuánto ha tardado el backend. Es el paso más importante del diagnóstico, porque responde a la pregunta "¿aplicación o línea?" sin necesidad de suponer nada. En el bloque http de /etc/nginx/nginx.conf:

log_format kh_timing '$time_iso8601 $status rt=$request_time '
                     'uct=$upstream_connect_time uht=$upstream_header_time '
                     'urt=$upstream_response_time "$request"';

En el bloque server afectado, una segunda línea de log. El log de acceso que ya existe se queda intacto, nginx escribe los dos:

access_log /var/log/nginx/timing.log kh_timing;
nginx -t
systemctl reload nginx
tail -n 5 /var/log/nginx/timing.log

Después de unos minutos de funcionamiento, saca arriba las peticiones más lentas:

awk '{ t=$3; sub(/^rt=/, "", t); print t, $0 }' /var/log/nginx/timing.log | sort -rn | head -20
ObservaciónInterpretaciónSiguiente paso
uct alto con un backend localEl establecimiento de la conexión se atascaCola llena en el socket, resolución de nombres lenta
uht y urt casi iguales, los dos altosEl backend calcula antes de enviar la primera cabeceraAplicación, base de datos, API externa
uht bajo, urt altoLa cabecera llegó rápido, el cuerpo va a gotasStreaming, exportaciones, bucles sobre muchos registros
urt bajo, rt altoEl backend fue rápido, el tiempo se perdió despuésLa línea del visitante, una respuesta muy grande
Un guion en lugar de un númeroNo intervino ningún backendArchivo estático o corte antes de pasar la petición

Dos detalles ahorran mucho tiempo. Varios valores separados por coma dentro de un mismo campo significan que la petición fue a más de un destino, es decir, que hubo un reintento. Y la pista más contundente de todas: si la duración medida coincide al segundo con el valor configurado, por ejemplo 60,001 segundos con un plazo de 60, entonces ha saltado el plazo y el backend no se ha rendido por su cuenta. Valores irregulares como 43,7 segundos indican que lo que frenó fue otra cosa.

Qué plazo se aplica realmente

nginx tiene más de una docena de directivas para los timeouts, y la hora perdida más habitual nace de tocar la que no es. Cuál se aplica depende de qué módulo procesa el bloque location.

DirectivaValor por defectoSe aplica en bloques conEfecto al vencer
proxy_connect_timeout60sproxy_pass504, "while connecting to upstream"
proxy_send_timeout60sproxy_pass504, "while sending request to upstream"
proxy_read_timeout60sproxy_pass504, el plazo decisivo con backends por proxy
fastcgi_connect_timeout60sfastcgi_pass504, igual que arriba, para PHP-FPM
fastcgi_send_timeout60sfastcgi_pass504, igual que arriba
fastcgi_read_timeout60sfastcgi_pass504, el plazo decisivo con PHP
send_timeout60sen todas partesningún 504, se cierra la conexión con el visitante
client_body_timeout60sen todas partes408, no 504

El plazo cuenta entre dos lecturas, no para la respuesta completa. Una descarga que dura diez minutos y que va entregando datos sin interrupción llega hasta el final. Un backend que se calla durante 61 segundos queda fuera. Por eso, en exportaciones que se atascan suele ayudar más hacer que la aplicación emita algo con regularidad que subir el plazo.

send_timeout no hace nada contra un 504. Ese plazo afecta a la transmisión hacia el visitante. Si vence, no hay página de error, sino una descarga cortada. Solo pasa a ser relevante cuando desactivas el búfer intermedio con proxy_buffering off;, porque entonces un visitante lento frena hasta el backend.

nginx también acepta directivas que no tienen ningún efecto en el bloque donde están. Un proxy_read_timeout 300s; dentro de un bloque PHP con fastcgi_pass es sintácticamente impecable, nginx -t informa syntax is ok y la página sigue cortándose a los 60 segundos. Al revés, exactamente igual. Es la causa más frecuente de que un plazo ampliado no sirva de nada. Lo que se aplica de verdad lo enseña la configuración ensamblada:

nginx -T | grep -E "read_timeout|send_timeout|fastcgi_pass|proxy_pass"

Si una ruta concreta tiene que correr más tiempo de verdad, pon el plazo exactamente ahí y en ningún otro sitio. El signo de igual lo convierte en una coincidencia exacta, y esa gana frente al bloque general location ~ \.php$, que procesa el resto de los archivos PHP:

location = /admin/export.php {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    fastcgi_read_timeout 300s;
}
nginx -t
systemctl reload nginx
curl -s -o /dev/null -w "%{http_code} %{time_total}\n" -H "Host: example.com" http://127.0.0.1/admin/export.php

PHP: por qué max_execution_time rara vez se aplica aquí

La suposición más inmediata es que PHP termina por sí solo un script colgado. En este caso concreto, casi nunca es así. Primero, la trampa de medición: php -i consulta la variante de línea de comandos. Esa usa una configuración propia bajo /etc/php/8.4/cli/ y de todas formas corre sin límite de tiempo de ejecución, así que no dice nada sobre FPM. Lo que cuenta son estos dos sitios:

grep -n "^max_execution_time" /etc/php/8.4/fpm/php.ini
grep -rn "max_execution_time" /etc/php/8.4/fpm/pool.d/

En el archivo de pool, el valor puede estar sobrescrito con php_value[max_execution_time] o con php_admin_value[max_execution_time]. Un valor puesto con php_admin_value ya no se puede cambiar desde la aplicación mediante ini_set(). Si tu framework sube el tiempo de ejecución por su cuenta y de repente eso no surte efecto, la razón es esa.

Y ahora, el punto que de verdad importa: en Linux, el tiempo de espera dentro de las llamadas al sistema no cuenta. El reloj solo corre mientras el propio script calcula. Si espera a una consulta de base de datos, a una API externa o al sistema de archivos, el reloj se para. Con eso, un script puede quedarse pegado diez minutos a una consulta colgada sin que el límite de tiempo de ejecución salte nunca. Quien lo termina entonces es el plazo de nginx, y el resultado es el 504.

De ahí sale una regla incómoda: max_execution_time protege frente a bucles infinitos en tu propio código, no frente a la espera. El único límite duro del lado de PHP es request_terminate_timeout en el archivo de pool, que se lleva por delante el proceso de trabajo sin importar de qué esté colgado. Eso sí, produce un 502 y no un 504. Ordena los plazos de dentro hacia fuera de forma creciente, para que actúe primero la capa que todavía puede generar un mensaje comprensible. Con los scripts que calculan funciona; con los que esperan no, por el motivo que acabamos de ver. Ahí solo queda limitar la propia espera.

Por qué subir el plazo suele ser la respuesta equivocada

Echa la cuenta un momento. Un pool con pm.max_children = 10 tiene diez procesos de trabajo. Una página necesita 90 segundos. Diez llamadas simultáneas ocupan así cada uno de ellos durante minuto y medio. En ese tiempo, nadie recibe ya ninguna página PHP, tampoco la portada. Una subpágina lenta se ha convertido en una caída. Hay tres mecanismos que lo agravan:

  • El visitante recarga. Eso genera una petición adicional, pero no libera la anterior. PHP no se entera de que un visitante ha cortado hasta que el script vuelve a emitir algo, y un script que está calculando no emite nada durante mucho rato.
  • nginx reintenta por su cuenta. En un bloque upstream con varios destinos, proxy_next_upstream viene de fábrica en error timeout. Una petición que ha vencido se va al siguiente servidor y la consulta cara se ejecuta una segunda vez. Las peticiones de escritura quedan excluidas, las de lectura no. Se desactiva con proxy_next_upstream error;.
  • La monitorización también repite. Un intervalo de comprobación de 60 segundos sobre una página que necesita 90 genera una carga permanente que nunca llega a despacharse.

A eso se suma que nadie espera cinco minutos por una página web. Un plazo de 300 segundos convierte un problema de un minuto en uno de cinco, y el visitante hace rato que se ha ido mientras el proceso de trabajo sigue calculando.

Hasta qué punto está realmente saturado lo enseña la página de estado de PHP-FPM. Pon pm.status_path = /fpm-status en el archivo de pool y crea en el bloque server un acceso alcanzable solo en local:

location = /fpm-status {
    allow 127.0.0.1;
    deny all;
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}
systemctl reload php8.4-fpm
nginx -t
systemctl reload nginx
curl -s -H "Host: example.com" http://127.0.0.1/fpm-status

Fíjate en active processes, listen queue y max active processes. Si listen queue se queda de forma permanente por encima de cero, el número de procesos de trabajo no da abasto para el tiempo de ejecución actual de las páginas. Ese es el momento en el que hay que bajar el tiempo de ejecución, no subir el plazo.

Los tres devoradores de tiempo habituales

Base de datos

En la mayoría de los casos, el tiempo está aquí. Mira primero qué se está ejecutando en ese momento. En los cuatro sistemas, el acceso de root funciona de fábrica a través del socket Unix, así que el comando no necesita contraseña:

mysql -e "SHOW FULL PROCESSLIST;"

Interesan las columnas Time y State. Valores como Sending data o Waiting for table metadata lock con segundos de dos cifras son tu caso. Para buscar de forma sistemática está el log de consultas lentas, que se puede activar en caliente:

mysql -e "SET GLOBAL slow_query_log = 1; SET GLOBAL long_query_time = 1;"
mysql -e "SHOW VARIABLES LIKE 'slow_query_log_file';"

El nombre del archivo lo sacas de la segunda salida, porque cambia de un sistema a otro: Debian apuesta normalmente por MariaDB y escribe en /var/log/mysql/mariadb-slow.log, mientras que en Ubuntu el archivo se llama de otra forma según el servidor instalado. Evalúalo después de unos minutos y vuelve a desactivarlo, porque el log cuesta carga de escritura:

mysqldumpslow -s t /var/log/mysql/mariadb-slow.log | head -30
mysql -e "SET GLOBAL slow_query_log = 0;"

El modificador -s t ordena por tiempo total. La consulta más cara la revisas con EXPLAIN: casi siempre falta un índice justo en la columna por la que se filtra o se ordena. SET GLOBAL surte efecto de inmediato, pero no sobrevive a un reinicio de la base de datos. Del lado de la base de datos también hay un límite superior por consulta: MariaDB tiene max_statement_time en segundos y MySQL max_execution_time en milisegundos, esto último solo para consultas de lectura. Con eso, tu aplicación recibe un error limpio en lugar de un proceso de trabajo ocupado.

APIs externas

Si tu página consulta en cada llamada a un proveedor de pagos o a un servidor de licencias, la avería de ellos se convierte en tu 504. Mide esa llamada por separado, desde el servidor:

curl -o /dev/null -s -w "dns=%{time_namelookup} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n" https://api.example.com/status

Si ya llama la atención dns, la culpa es de la resolución de nombres y no del otro extremo; contraprueba con time getent hosts api.example.com. En el código, cada llamada externa necesita un plazo propio y corto: en cURL son CURLOPT_CONNECTTIMEOUT y CURLOPT_TIMEOUT. Si en su lugar aplicas file_get_contents() sobre una dirección, acabas en default_socket_timeout del php.ini, donde de fábrica hay 60 segundos:

grep -n "default_socket_timeout" /etc/php/8.4/fpm/php.ini

La regla general: la suma de todos los plazos externos de una petición tiene que quedar por debajo de fastcgi_read_timeout. Si no, nginx corta antes de que tu código pueda mostrar una página de error comprensible.

Sistema de archivos y RAM

Un disco lleno hace que las escrituras vayan lentas o resulten imposibles, y eso afecta a la vez a los archivos de sesión, a la caché y a los logs. Comprueba las dos cosas, el espacio en disco y los inodos:

df -h
df -i

El segundo comando es el que se olvida a menudo. Una partición puede estar al 40 % de ocupación y aun así no admitir ni un archivo más si los inodos se han agotado. El segundo candidato es la falta de memoria: si el sistema empieza a tirar de swap, cada petición se vuelve lenta sin que la culpa sea de ninguna consulta concreta.

vmstat 1 5

Si las columnas si y so se mantienen permanentemente por encima de cero, el kernel está paginando sin parar hacia dentro y hacia fuera, y lo que tienes es un problema de memoria y no de tiempo. Cómo tratar eso bien lo explica configurar swap y evitar el Out-of-Memory. El tercer candidato son las unidades de red: un punto de montaje NFS colgado bloquea cualquier proceso que lo toque, y ahí entra también tu comando de diagnóstico. Por eso, ponle un plazo delante:

timeout 5 df -h

Las tareas largas no pintan nada dentro de la petición

Hay tareas que sencillamente tardan: un informe anual, una importación de 200.000 filas, una conversión de imágenes. El fallo no está en que tarden mucho, sino en que un servidor web se quede esperándolas. La forma limpia tiene tres partes:

  1. La petición crea un trabajo, en una tabla o en una cola, y responde de inmediato. El código de estado que corresponde es el 202, junto con una dirección desde la que se puede consultar el estado.
  2. Un proceso de trabajo fuera de nginx recoge esos trabajos y los resuelve. No tiene ningún plazo pisándole los talones, porque nadie lo está esperando.
  3. La interfaz consulta el estado. Esa consulta es siempre rápida, dure lo que dure la tarea.

El proceso de trabajo lo ejecutas como servicio propio, para que vuelva por sí solo después de una caída y después de un reinicio. Qué aspecto tiene una unit así lo enseña crear un servicio systemd. Para tareas a intervalos fijos basta con un cronjob, y ahí necesitas un bloqueo para que dos ejecuciones no se adelanten la una a la otra. Con -n, la segunda ejecución aborta al momento en lugar de esperar:

flock -n /run/lock/kh-worker.lock /usr/bin/php /var/www/html/worker.php

Para los frameworks habituales, la cola ya viene hecha y solo hay que ponerla en marcha: en Laravel php artisan queue:work, en Symfony php bin/console messenger:consume con el nombre de tu transporte. Las dos cosas van en una unit de systemd, no en una ventana de terminal.

Un caso especial merece una mención expresa: WordPress lanza de fábrica las tareas programadas dentro de las peticiones de los visitantes. Es decir, un visitante paga con su tiempo de espera que por detrás se esté ejecutando una comprobación de actualizaciones. La línea define('DISABLE_WP_CRON', true); en wp-config.php, por encima de la referencia a wp-settings.php, lo desactiva. Después, las tareas pendientes las lanzas tú mismo con regularidad:

wp cron event run --due-now --path=/var/www/html

Lo que tenga que seguir siendo síncrono por fuerza recibe un bloque location propio con su propio plazo y, además, un límite para que esa única ruta no ocupe todos los procesos de trabajo: limit_conn_zone $binary_remote_addr zone=export:10m; en el bloque http y limit_conn export 1; en el bloque correspondiente. Los intentos adicionales reciben entonces un 503 en lugar de un servidor ocupado.

Errores frecuentes y soluciones

Mensaje literalSignificado y solución
upstream timed out (110: Connection timed out) while reading response header from upstreamEl caso normal. El backend calcula demasiado tiempo. Evalúa el log de tiempos y determina el devorador de tiempo antes de tocar el plazo.
upstream timed out (110: Connection timed out) while connecting to upstreamEl establecimiento de la conexión agotó el plazo. Con un backend remoto es casi siempre un filtro de paquetes que descarta en vez de rechazar; con PHP-FPM local, una cola llena en el socket.
upstream timed out (110: Connection timed out) while reading upstreamLas cabeceras llegaron y después el cuerpo se atascó más tiempo que el plazo. Típico de exportaciones que entre medias se ponen a calcular largo rato.
nginx: [emerg] "fastcgi_read_timeout" directive is not allowed here in /etc/nginx/nginx.conf:12La directiva está fuera de http, server o location, casi siempre por descuido en lo más alto del archivo.
nginx: [emerg] unknown directive "proxy_read_timout" in /etc/nginx/sites-enabled/example.com:31Error de tecleo. nginx comprueba nombres, no intenciones. El número de línea está en el mensaje.
PHP Fatal error: Maximum execution time of 30 seconds exceeded in /var/www/html/export.php on line 42Aquí sí ha actuado, por una vez, el límite de tiempo de ejecución de PHP, así que el script estaba calculando y no esperando. Da un 500 o una página en blanco, no un 504.
SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transactionOtra transacción retiene la fila; de fábrica, ese plazo está en 50 segundos. La causa es casi siempre una transacción que lleva demasiado tiempo abierta.
cURL error 28: Operation timed out after 60000 millisecondsUna API externa no responde. Pon en el código un plazo propio más corto para que tu aplicación mantenga el control.
504 en el navegador, pero la búsqueda de upstream timed out se queda vacíaEl 504 no viene de este nginx, sino de un servicio situado por delante, como un balanceador de carga o un segundo proxy. Algunos informan de ello con un código de estado propio.
El corte sigue produciéndose a los 60 segundos exactos aunque el plazo esté en 300La directiva modificada pertenece al módulo equivocado, o gana otro bloque. nginx -T enseña lo que se aplica de verdad.

Cómo saber que está resuelto

Un comando sin mensaje de error no demuestra nada, y una sola llamada con éxito tampoco. Cuatro pruebas que, juntas, sí sostienen:

  1. Código de estado y duración directamente en el servidor, para que ninguna caché maquille el resultado. Se espera un 200 y una duración claramente por debajo del plazo. Un 200 a los 58 segundos con un plazo de 60 no es un éxito, sino la próxima caída en cuanto suba algo la carga. Y aquí cuenta la peor de veinte llamadas:
    for i in $(seq 1 20); do curl -s -o /dev/null -w "%{http_code} %{time_total}\n" -H "Host: example.com" http://127.0.0.1/report.php; done | sort -k2 -n | tail -3
  2. El log de errores se queda callado. Vacíalo antes de la prueba con truncate -s 0 /var/log/nginx/error.log, lanza las llamadas y vuelve a mirar dentro. Un archivo vacío es la prueba de verdad.
  3. La cola de PHP-FPM está a cero. Mientras en la página de estado haya algo esperando bajo listen queue, la causa solo se ha desplazado.
  4. Un reinicio no cambia nada. El punto que más se salta la gente. Los valores puestos con SET GLOBAL, los procesos arrancados a mano y los directorios creados por ti mismo bajo /run no lo sobreviven. Comprueba systemctl is-enabled nginx php8.4-fpm y reinicia el servidor una vez de forma controlada, mientras todavía estás delante mirando.

Ese reinicio, con acceso a consola incluido, lo haces en los servidores root KVM y en los servidores dedicados de KernelHost desde el área de cliente, incluso cuando el servicio web ya no entrega nada. Los servidores están en el centro de datos maincubes de Frankfurt am Main (certificado TÜV TIER3+), con filtrado previo en la red que tienen por delante.

Lista de comprobación rápida para la emergencia

  1. Contar los códigos de estado en el log de acceso. El 504, el 499 y el 502 juntos dicen más que cualquiera de ellos por separado.
  2. grep "upstream timed out" /var/log/nginx/error.log y anotar la fase que aparece en el mensaje.
  3. Activar el log de tiempos y comparar uct, uht y urt. Si la duración encaja al segundo con el plazo configurado, es que ha saltado el plazo.
  4. Determinar el devorador de tiempo: base de datos, API externa, sistema de archivos o memoria.
  5. Comprobar con nginx -T qué plazo se aplica en ese bloque antes de cambiar ninguno.
  6. Solo después decidir: arreglarlo, moverlo a una cola o, como última opción, subir el plazo solo para esa única ruta.
  7. Después del arreglo: vaciar el log, medir veinte llamadas y reiniciar el servidor una vez de forma controlada.

Preguntas frecuentes

¿Cuál es la diferencia entre 502 Bad Gateway y 504 Gateway Time-out?
En el 502, la conexión con el backend no llega a establecerse o se corta, así que el backend responde mal o no responde en absoluto. En el 504 la conexión estaba en pie y lo único que pasó es que la respuesta no llegó dentro del plazo: el backend estuvo accesible todo el tiempo y respondió demasiado despacio. Un 500, en cambio, significa que el backend sí respondió y que la respuesta fue un error. De ahí sale la dirección en la que hay que buscar: en el 502 revisas el servicio, el socket y los permisos; en el 504, el tiempo de ejecución en el backend.
He puesto fastcgi_read_timeout en 300 segundos y aun así la página se corta a los 60 segundos. ¿A qué se debe?
Casi siempre a que la directiva modificada pertenece al módulo equivocado o a que gana otro bloque. nginx también acepta directivas que no tienen ningún efecto en el bloque donde están: un proxy_read_timeout 300s; dentro de un bloque PHP con fastcgi_pass es sintácticamente impecable, nginx -t informa "syntax is ok" y la página sigue cortándose a los 60 segundos. Al revés, exactamente igual. Lo que se aplica de verdad lo enseña la configuración ensamblada con nginx -T y una búsqueda de read_timeout, send_timeout, fastcgi_pass y proxy_pass. Si una ruta concreta tiene que correr más tiempo, pon el plazo en un bloque location propio con signo de igual, porque una coincidencia exacta gana frente al bloque PHP general.
¿Un plazo más alto resuelve el problema?
Casi siempre se limita a aplazarlo. Un pool con pm.max_children = 10 tiene diez procesos de trabajo. Si una página necesita 90 segundos, diez llamadas simultáneas ocupan cada uno de ellos durante minuto y medio, y en ese tiempo nadie recibe ya ninguna página PHP, tampoco la portada. Una subpágina lenta se ha convertido así en una caída. A eso se suma que nadie espera cinco minutos por una página web. Si el 504 desaparece después de subir el plazo y en su lugar aparecen 499 en el log de acceso, quien ha cortado es el propio visitante y no se ha resuelto nada.
¿Qué línea del log de errores corresponde a un 504?
Un 504 deja siempre una línea de la forma "upstream timed out (110: Connection timed out) while reading response header from upstream", que encuentras con grep -n "upstream timed out" /var/log/nginx/error.log. Lo decisivo no es el número de error 110, que es el mismo en todos los 504, sino la fase que viene detrás: "while connecting to upstream" significa que el establecimiento de la conexión no llegó a producirse; "while sending request to upstream", que nginx no consiguió soltar el cuerpo de la petición; "while reading response header from upstream" es el caso normal, en el que el backend calcula sin enviar siquiera la primera línea de cabecera; y "while reading upstream" quiere decir que después de las cabeceras se atascó el cuerpo.
¿Por qué max_execution_time no termina mi script PHP colgado?
Porque en Linux el tiempo de espera dentro de las llamadas al sistema no cuenta. El reloj solo corre mientras el propio script calcula. Si espera a una consulta de base de datos, a una API externa o al sistema de archivos, el reloj se para, y el script puede quedarse pegado diez minutos a una consulta colgada sin que el límite de tiempo de ejecución salte nunca. Quien lo termina entonces es el plazo de nginx, y el resultado es el 504. Ten en cuenta además la trampa de medición: php -i consulta la variante de línea de comandos, que usa una configuración propia y que de todas formas corre sin límite de tiempo de ejecución. Lo que cuenta son el php.ini de FPM y el archivo de pool. El único límite duro del lado de PHP es request_terminate_timeout, aunque este produce un 502 y no un 504.
¿Cómo distingo si ha saltado el plazo o si el backend se ha rendido por su cuenta?
Por la duración medida. Registra con un log_format propio los valores $request_time, $upstream_connect_time, $upstream_header_time y $upstream_response_time en un archivo adicional. Si la duración coincide al segundo con el valor configurado, por ejemplo 60,001 segundos con un plazo de 60, entonces ha saltado el plazo y el backend no se ha rendido por su cuenta. Valores irregulares como 43,7 segundos indican que lo que frenó fue otra cosa. Varios valores separados por coma dentro de un mismo campo significan un reintento contra un segundo destino, y un guion en lugar de un número significa que no intervino ningún backend.
El navegador muestra un 504, pero la búsqueda de "upstream timed out" se queda vacía. ¿De dónde viene el error?
Entonces el 504 no viene de este nginx, sino de un servicio situado por delante, como un balanceador de carga o un segundo proxy, y algunos informan de ello con un código de estado propio. Antes de llegar ahí, comprueba una cosa más: muchos hosts virtuales escriben en un log de errores propio, así que es posible que sencillamente estés buscando en el archivo equivocado.
¿Qué hago con las tareas que por naturaleza duran más que cualquier plazo razonable?
Sácalas de la petición. La petición crea un trabajo y responde de inmediato, con el código de estado 202 y una dirección desde la que se puede consultar el estado. Un proceso de trabajo fuera de nginx resuelve ese trabajo sin ningún plazo pisándole los talones, y la interfaz se limita a consultar el estado. Ejecuta ese proceso de trabajo como servicio propio, para que vuelva por sí solo después de una caída y después de un reinicio, y protege las ejecuciones recurrentes con flock -n para que dos ejecuciones no se adelanten la una a la otra. Lo que tenga que seguir siendo síncrono recibe un bloque location propio con su propio plazo y, además, un límite mediante limit_conn para que esa única ruta no ocupe todos los procesos de trabajo.

nginx PHP-FPM 504 Gateway Time-out Timeout Debian Ubuntu Solución de errores Administración de Linux