Medir cada salto
curl -sS -o /dev/null -w 'connect=%{time_connect} start=%{time_starttransfer} total=%{time_total}\n' https://tu-dominio.com
curl -v http://127.0.0.1:3000/ruta-lenta Compara la URL pública con el backend directo. Revisa logs con la misma marca de tiempo.
Causas frecuentes
- Consulta SQL sin índice o bloqueada.
- API externa lenta.
- Cola de workers llena.
- CPU, RAM o disco saturados.
- Resolución DNS o red entre proxy y upstream.
- Trabajo síncrono que debería ejecutarse en segundo plano.
Usa las herramientas de CPU y RAM y los logs de aplicación para confirmar.
Timeouts de Nginx
Para una operación legítimamente larga puedes ajustar en la ubicación apropiada:
proxy_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
send_timeout 60s; Valida con sudo nginx -t y recarga. No conviertas 60 segundos en 10 minutos sin entender la carga; cada conexión retenida consume capacidad y la CDN puede tener un límite menor que no controlas.
Solución de arquitectura
Exportaciones, video y procesamiento pesado suelen funcionar mejor como trabajos asíncronos: la solicitud crea una tarea, un worker la procesa y el usuario consulta el resultado. Añade índices, limita concurrencia y establece timeouts explícitos hacia APIs.
504 detrás de Docker
Comprueba salud y recursos con docker compose ps, docker stats y logs. Asegura que el proxy usa el nombre y puerto interno correctos. Un contenedor “Up” puede tener la aplicación bloqueada; añade healthchecks reales.
Dibujar la cadena que atendió la solicitud
Un 504 puede atravesar varias capas, y debes encontrar cuál dejó de esperar. Una ruta pública puede ser navegador → CDN → balanceador → Nginx → aplicación → base de datos → API externa. Cada salto tiene sus propios límites.
Anota el código, hora, URL, método y un identificador de solicitud si existe. Captura cabeceras:
curl -sv -o /dev/null https://tu-dominio.com/ruta-lenta 2>&1 Encabezados como Server, Via o identificadores de la CDN ayudan, pero pueden ocultarse o personalizarse. Confirma con los logs. Si Nginx registra 504 en su access.log, participó en la respuesta; si no ve la solicitud, el timeout ocurrió antes de llegar al origen.
No desactives la CDN como primer reflejo. Prueba el origen de forma controlada y conserva protección; un cambio DNS improvisado puede añadir otra avería.
Registrar tiempo total y tiempo del upstream
Sin tiempos en el log, solo sabes que la solicitud falló, no cuánto esperó Nginx ni cuánto tardó el backend. Añade un formato que incluya ambas métricas:
log_format upstream_timing '$remote_addr - $host [$time_local] '
'"$request" $status bytes=$body_bytes_sent '
'request_time=$request_time '
'upstream_addr=$upstream_addr '
'upstream_status=$upstream_status '
'upstream_connect=$upstream_connect_time '
'upstream_header=$upstream_header_time '
'upstream_response=$upstream_response_time '
'request_id=$request_id';
access_log /var/log/nginx/access.log upstream_timing; Valida y recarga:
sudo nginx -t
sudo systemctl reload nginx
sudo tail -f /var/log/nginx/access.log upstream_connect_time alto apunta a establecer conexión; upstream_header_time alto indica que el backend tardó en producir cabeceras; request_time incluye toda la operación vista por Nginx. Valores separados permiten decidir si investigar red, cola o procesamiento.
Comparar la ruta pública con el backend directo
La misma solicitud debe probarse en cada salto con el mismo método y datos seguros. Mide URL pública:
curl -sS -o /dev/null \
-w 'code=%{http_code} connect=%{time_connect} start=%{time_starttransfer} total=%{time_total}\n' \
https://tu-dominio.com/reporte Después prueba el upstream desde el VPS:
curl -sS -o /dev/null \
-H 'Host: tu-dominio.com' \
-w 'code=%{http_code} connect=%{time_connect} start=%{time_starttransfer} total=%{time_total}\n' \
http://127.0.0.1:3000/reporte Si el backend directo tarda lo mismo, investiga la aplicación y sus dependencias. Si responde rápido pero el público falla, revisa el proxy intermedio, DNS, TLS y diferencias de encabezados. No publiques tokens o datos personales en el comando; reproduce con una cuenta de prueba.
Una petición GET /health rápida no exonera una ruta de reporte. Prueba la operación afectada y conserva una ruta de salud barata para saber si todo el proceso está bloqueado.
Distinguir conexión lenta de respuesta lenta
Los timeouts de Nginx controlan fases distintas y no son intercambiables. En proxy HTTP:
-
proxy_connect_timeout: tiempo para conectar con upstream. -
proxy_send_timeout: espera entre operaciones al enviar la solicitud. -
proxy_read_timeout: espera entre lecturas de la respuesta del upstream. -
send_timeout: espera al enviar la respuesta al cliente.
Una configuración limitada a la ubicación larga puede ser:
location /exportaciones/ {
proxy_pass http://127.0.0.1:3000;
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 120s;
send_timeout 60s;
} proxy_read_timeout no es necesariamente un cronómetro absoluto de toda la respuesta; se aplica entre operaciones de lectura. Aun así, una solicitud que retiene un worker y conexión durante minutos consume capacidad. Establece el valor a partir de un objetivo y no como infinito práctico.
Para PHP-FPM la directiva relevante es fastcgi_read_timeout, no proxy_read_timeout. Cada protocolo tiene opciones distintas; confirma si tu bloque usa proxy_pass, fastcgi_pass o uwsgi_pass antes de editar.
Encontrar una cola de workers saturada
Cuando llegan más solicitudes de las que los workers completan, las nuevas esperan hasta superar el timeout aunque CPU no esté al 100 %. Revisa procesos, conexiones y logs del runtime:
sudo ss -tanp | grep ':3000'
ps -eo pid,ppid,stat,%cpu,%mem,etime,comm,args --sort=-%cpu | head -30
sudo journalctl -u miapp --since '20 minutes ago' Un worker bloqueado en I/O puede usar poca CPU. Compara concurrencia configurada, solicitudes en curso y tiempo de servicio. Aumentar workers puede ayudar si existe CPU y memoria, pero también multiplicar conexiones a la base hasta saturarla.
Para Gunicorn revisa workers y timeout propios; para PHP-FPM observa pm.max_children y el log de procesos lentos; para Node identifica trabajo síncrono que bloquea el event loop. La métrica adecuada depende del runtime, pero el patrón de cola es el mismo.
Diagnosticar consultas lentas y bloqueos en la base
Una ruta que espera a la base puede agotar Nginx aunque la aplicación esté saludable. Revisa sesiones activas, consultas largas y bloqueos con herramientas del motor. En PostgreSQL, una consulta de observación es:
SELECT pid,
now() - query_start AS duration,
wait_event_type,
wait_event,
state,
left(query, 120) AS query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY query_start; En MySQL puedes empezar con:
SHOW FULL PROCESSLIST;
SHOW ENGINE INNODB STATUS; No mates una consulta sin saber si mantiene una transacción necesaria. Identifica la ruta, el plan y el bloqueo. Una consulta sin índice puede mejorar con un índice validado; una transacción olvidada necesita corregir el código; un pool agotado puede deberse a conexiones que no se liberan.
Mide con datos representativos. Una consulta rápida en una base vacía no reproduce una exportación de producción.
Medir dependencias externas y DNS
Si la aplicación llama a pagos, correo o una API, su timeout también forma parte de la latencia total. Una dependencia sin límite puede retener el worker hasta que Nginx se rinda. Desde el mismo entorno que la aplicación:
curl -sS -o /dev/null \
-w 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} start=%{time_starttransfer} total=%{time_total}\n' \
https://api.ejemplo.com/health Configura timeouts explícitos de conexión y lectura en el cliente HTTP de la aplicación. Define qué hacer cuando la dependencia falla: reintentar solo operaciones idempotentes, usar una cola o devolver un error controlado. Reintentos inmediatos sin límite multiplican tráfico durante una avería y prolongan el 504.
Si DNS es lento, revisa resolución desde el contenedor o servicio, no solo desde el host. Cachés y servidores configurados pueden ser distintos.
Revisar CPU, memoria y disco durante el timeout
Los recursos deben medirse durante la solicitud problemática. Ejecuta en otra terminal:
vmstat 1
pidstat -u -r -d 1
iostat -xz 1 Relaciona la marca de tiempo con el log. CPU saturada sugiere procesamiento; iowait y latencia alta apuntan a disco; swap intensa y OOM apuntan a memoria. Un disco lleno puede bloquear escrituras de base y logs, por lo que incluye:
df -h
df -i
sudo journalctl -k --since '30 minutes ago' | grep -Ei 'oom|killed process|I/O error' No escales solo una métrica. Añadir CPU a un bloqueo SQL no libera la transacción; ampliar RAM no corrige una API sin timeout.
504 en Docker y redes internas
Un contenedor en estado Up puede tener su aplicación bloqueada o un healthcheck insuficiente. Comprueba servicio, recursos y solicitud desde la red del proxy:
docker compose ps
docker stats --no-stream
docker compose logs --since=20m app
docker exec nginx wget -S -O- http://app:3000/health Si el proxy resuelve app pero la solicitud tarda, investiga el proceso. Si no resuelve, confirma que ambos servicios comparten red. No uses una IP de contenedor fija: cambia al recrear.
Revisa límites:
docker inspect app \
--format 'memory={{.HostConfig.Memory}} cpus={{.HostConfig.NanoCpus}} oom={{.State.OOMKilled}}' Un límite de CPU puede alargar una tarea hasta superar el timeout aunque el host tenga capacidad libre. Ajustarlo requiere entender por qué existe y cómo se reparte entre servicios.
No ignorar el límite de una CDN o balanceador
Un proxy externo puede tener un timeout máximo que tu configuración de Nginx no controla. Si el origen tarda 90 segundos y la CDN corta antes, aumentar proxy_read_timeout a cinco minutos no cambia la experiencia pública.
Prueba el origen directamente usando su IP sin modificar DNS:
curl --resolve tu-dominio.com:443:IP_DEL_ORIGEN \
-sS -o /dev/null \
-w 'code=%{http_code} start=%{time_starttransfer} total=%{time_total}\n' \
https://tu-dominio.com/ruta-lenta Compara con la ruta a través de CDN. Consulta la documentación vigente del proveedor para sus límites; no inventes una cifra universal. Si la operación excede el máximo, rediseñarla como tarea asíncrona es más fiable que buscar una directiva oculta.
Mover trabajos largos fuera de la solicitud HTTP
Reportes, importaciones y procesamiento multimedia no deberían retener una conexión mientras completan todo el trabajo. Un patrón asíncrono es:
- 1 El cliente envía la solicitud.
- 2 La aplicación valida y crea un trabajo en cola.
- 3 Responde
202 Acceptedcon un identificador. - 4 Un worker procesa y guarda el resultado.
- 5 El cliente consulta estado o recibe una notificación.
Esto separa el timeout web del tiempo de procesamiento y permite reintentos controlados. La cola necesita límites, visibilidad, idempotencia y manejo de trabajos fallidos; no consiste solo en “mandarlo al fondo”.
Para una descarga grande ya generada, Nginx puede servir el archivo o redirigir a almacenamiento de objetos. Evita que el runtime reconstruya el mismo reporte en cada intento del navegador.
Ajustar tiempos con un presupuesto de latencia
Los límites deben ser coherentes de dentro hacia fuera. La consulta a base y la API externa deberían expirar antes que la aplicación; la aplicación antes que Nginx; y el proxy de borde después de Nginx si la plataforma lo permite. Así cada capa devuelve un error controlado y deja margen para registrarlo.
Un ejemplo conceptual para una ruta de 30 segundos:
| Operación | Límite interno |
|---|---|
| Conectar a base | 2 s |
| Consulta principal | 15 s |
| API externa | 5 s |
| Procesamiento de aplicación | 25 s |
| Lectura de Nginx | 30 s |
| Proxy externo | Mayor a 30 s o trabajo asíncrono |
Estas cifras son un ejemplo de relación, no valores para copiar. Usa el percentil de latencia de tu servicio, el objetivo del negocio y los límites reales de proveedores.
Validar la corrección bajo una carga comparable
Una solicitud exitosa después del cambio no demuestra que la cola desapareció. Repite varias veces o usa una prueba de carga autorizada desde un entorno controlado. Observa latencia, errores, workers, conexiones de base y recursos.
for i in $(seq 1 10); do
curl -sS -o /dev/null \
-w "$i code=%{http_code} start=%{time_starttransfer} total=%{time_total}\n" \
https://tu-dominio.com/ruta-de-prueba
done No ejecutes una carga agresiva contra producción sin autorización. El bucle anterior es una muestra pequeña; adapta frecuencia y ruta para no generar datos reales ni tareas costosas.
Compara con la línea base y confirma que los logs ya no muestran upstream timed out. Si aumentaste un timeout, registra también cuántas conexiones quedan abiertas y cuánta memoria consumen.
Prevenir la repetición del 504
La prevención consiste en observar latencia antes de que alcance el límite. Registra tiempo total y upstream, crea alertas por percentiles y tasa de 504, mide cola de workers y consultas lentas, y relaciona despliegues con degradación.
Define una ruta de salud que sea rápida y otra comprobación funcional de dependencias para monitoreo, sin convertir cada healthcheck en una consulta costosa. Limita concurrencia de tareas pesadas, programa reportes y usa caché solo cuando sus datos y caducidad sean correctos.
El resultado esperado no es “Nginx espera más”, sino que la operación completa dentro del presupuesto o continúa fuera de la solicitud con un estado visible para el usuario.
Revisar el pool de conexiones de la aplicación
Un pool agotado hace que las solicitudes esperen una conexión libre aunque la base responda rápidamente a quien ya está conectado. Registra tamaño del pool, conexiones activas y tiempo de espera. Compara con el máximo de la base; multiplicar workers por conexiones de cada uno puede superar ese límite.
No aumentes el pool en todas las instancias a la vez. Más conexiones compiten por memoria y bloqueos. Corrige conexiones que no se liberan, define un timeout de adquisición inferior al de Nginx y limita concurrencia de rutas pesadas.
Si una solicitud espera al pool, el log debería distinguir ese tiempo del tiempo de consulta. Instrumenta ambas fases; de lo contrario una “consulta de 20 segundos” puede ser un segundo de SQL después de diecinueve esperando turno.
Tratar cancelaciones y reintentos del cliente
Cuando el navegador abandona una solicitud, el trabajo del backend puede continuar y consumir recursos si no propaga la cancelación. Reintentar varias veces una exportación lenta puede dejar varios trabajos idénticos ejecutándose aunque el usuario solo vea un 504.
Usa identificadores idempotentes para operaciones que crean datos y cancela consultas cuando la conexión ya no necesita el resultado, si el framework lo permite. Los reintentos automáticos deben tener límite, espera creciente y aplicarse solo cuando repetir sea seguro.
Nginx puede registrar estados especiales cuando el cliente cierra antes de recibir respuesta. Relaciónalos con logs de aplicación y con la cola; no interpretes todo cierre como ataque o fallo de red.
Usar streaming solo cuando produce progreso real
Enviar cabeceras o fragmentos puede evitar que un proxy considere inactiva la respuesta, pero no acelera el trabajo ni supera todos los límites externos. Streaming es adecuado para eventos o una descarga generada progresivamente; no para mantener abierta una página mientras una consulta bloqueada no produce nada.
Prueba si el backend entrega datos de forma continua:
curl -N -v https://tu-dominio.com/stream-de-prueba Revisa buffering en la ubicación concreta y el comportamiento de CDN. Algunos intermediarios almacenan fragmentos o aplican una duración total. Si el usuario no necesita datos parciales, una tarea asíncrona con estado sigue siendo más robusta.
Preparar una respuesta controlada cuando una dependencia vence
La aplicación debería transformar el timeout interno en un error útil antes de que Nginx emita un 504 genérico. Define límites menores hacia base y APIs, captura la excepción y devuelve un código coherente o encola la operación. Registra dependencia, duración e identificador sin exponer secretos.
Después prueba una dependencia simuladamente lenta en staging. Confirma que los workers se liberan, que el usuario recibe respuesta y que el circuito se recupera. Sin esa prueba, los timeouts existen solo en configuración y pueden no cubrir el camino real.
Construir una matriz de límites de toda la ruta
Un solo valor no explica la cadena: documenta qué componente corta, en qué fase y con qué respuesta. Una tabla operativa puede incluir:
| Capa | Conexión | Lectura o trabajo | Evidencia |
|---|---|---|---|
| Cliente | Timeout del SDK | Tiempo total | Log o traza cliente |
| CDN | Límite del proveedor | Límite fijo o configurable | Evento de borde |
| Nginx | proxy_connect_timeout | proxy_read_timeout | Timing de upstream |
| Aplicación | Pool y HTTP saliente | Límite de handler | Traza por request ID |
| Base | Conexión | Statement o lock timeout | Sesiones y slow log |
Rellena valores reales desde configuración y documentación vigente. El límite interno debería vencer con tiempo suficiente para que la capa exterior devuelva un error controlado. Si el cliente abandona antes que todos, seguirá mostrando fallo aunque el origen complete.
Conservar un identificador a través de proxies
Un request ID une la entrada pública con logs de Nginx, aplicación y worker. Acepta uno confiable o genera $request_id, envíalo al upstream y devuélvelo para soporte:
proxy_set_header X-Request-ID $request_id;
add_header X-Request-ID $request_id always; La aplicación debe incluirlo en cada log de dependencia y tarea. No uses datos personales como identificador. Con esa correlación puedes demostrar que la solicitud pasó 200 ms en Nginx, 18 segundos esperando una conexión y 2 segundos en SQL, en lugar de tratar los 20 segundos como una consulta misteriosa.
Después de corregir, busca ese mismo ID y confirma que todas las capas cerraron. Una respuesta 200 al cliente con una tarea duplicada aún ejecutándose no es recuperación completa.
Separar carga de archivos de procesamiento posterior
Una subida puede consumir tiempo al recibir el cuerpo y después iniciar un procesamiento largo. Mide ambas fases. client_body_timeout controla la lectura desde el cliente; los timeouts del upstream afectan el envío hacia la aplicación y su respuesta. Aumentar solo proxy_read_timeout no ayuda si la conexión móvil tarda en subir.
Guarda el archivo de forma segura, valida tamaño y tipo, responde con un identificador y procesa después mediante cola cuando sea costoso. Evita cargar todo el contenido en RAM y establece límites coherentes en Nginx y la aplicación.
location /cargas/ {
client_max_body_size 100m;
client_body_timeout 60s;
proxy_request_buffering on;
proxy_pass http://127.0.0.1:3000;
} Los valores son un ejemplo, no una recomendación universal. proxy_request_buffering on hace que Nginx reciba el cuerpo antes de enviarlo al upstream, lo que protege workers pero usa almacenamiento temporal. Revisa espacio, permisos y experiencia de reintento.
Para archivos grandes considera subida directa a almacenamiento de objetos con una URL temporal. La aplicación autoriza y registra el objeto sin transportar todos los bytes; así un worker web no queda retenido por la velocidad del cliente.
Evitar una avalancha al caducar la caché
Muchas solicitudes pueden intentar regenerar el mismo recurso al mismo tiempo cuando vence una caché. Cada una ocupa un worker y ejecuta la consulta costosa, formando una cola que termina en 504. El pico ocurre aunque el tráfico total sea habitual.
Usa bloqueo de regeneración, expiraciones escalonadas o entrega temporal de contenido anterior cuando el dato lo permita. No caches respuestas personalizadas ni datos sensibles solo para reducir carga.
Relaciona la hora de expiración con consultas y workers. Si todas las latencias suben a intervalos regulares, revisa TTL y jobs de invalidación. Prueba en staging con varias solicitudes simultáneas: la meta es que una regenere y las demás reciban caché o esperen de forma limitada, no que todas golpeen la base.
Un 504 es una medida de tiempo, no un diagnóstico. Localiza el salto lento, mide el backend y corrige consulta, worker, red o capacidad. Aumenta el timeout solo cuando la duración es legítima y todas las capas lo admiten.
Hosting con LiteSpeed, NVMe y soporte 24/7 desde $3/mes.