Estás observando un panel. La latencia se dispara. Algunos contenedores reinician. Luego tu canal de incidentes se llena con las mismas dos palabras:
“request timeout”.
Alguien sugiere “simplemente aumentar los timeouts” y otra persona sugiere “añadir reintentos.” La tercera persona—siempre la tercera—sugiere “reintentos infinitos.”
Esa última es la forma de convertir una pequeña falla en una persistente.
Qué son realmente los timeouts (y por qué no son bugs)
Un timeout es una decisión. Es el momento en que tu sistema dice: “ya no espero más.” Esa decisión puede ocurrir en una librería cliente, un proxy inverso,
un service mesh, la pila TCP del kernel, un resolvedor DNS o un plano de control. A veces en todos ellos.
La trampa es tratar “timeout” como un único control. No lo es. “Timeout” es una familia de plazos que interactúan: timeout de conexión, timeout de lectura,
timeout de escritura, timeout de inactividad, timeout de keepalive, timeout de healthcheck, timeout de apagado suave, y así sucesivamente. Cada uno existe para limitar el uso de recursos
y contener la falla.
Si pones reintentos infinitos encima, eliminas la contención. El error deja de ser visible, pero la carga no desaparece—se desplaza a colas,
pools de conexiones, pilas de hilos y servicios aguas abajo. También pierdes la señal más importante durante un incidente: la tasa de fallos.
Tu objetivo no es “sin timeouts.” Tu objetivo es “timeouts que fallen rápido para solicitudes sin esperanza, que reintenten solo cuando sea seguro, y que dejen de reintentar antes
de que el sistema colapse.”
Chiste corto #1: Los reintentos infinitos son como gritar “¿YA LLEGAMOS?” en un viaje por carretera. No llegas más rápido; solo haces a todos más infelices.
En qué deberías optimizar
- Espera acotada: cada solicitud tiene una fecha límite. Pasada esa, descártala y sigue.
- Reintentos acotados: una política de reintentos es un presupuesto, no una esperanza.
- Backoff y jitter explícitos: para no crear tormentas de reintentos sincronizadas.
- Conciencia de idempotencia: no puedes reintentar todo de forma segura.
- Visibilidad de fallos: los errores deben aflorar lo bastante rápido como para activar mitigaciones.
Una cita que te mantenga honesto
Werner Vogels (idea parafraseada): “Todo falla; diseña para contener y recuperarte de la falla en lugar de fingir que no ocurrirá.”
Hechos e historia: cómo surgieron tantos timeouts
Los timeouts en sistemas containerizados no aparecieron porque los ingenieros empeoraran. Se multiplicaron porque los sistemas se volvieron más distribuidos, más estratificados
y más dependientes de redes que a veces se comportan como redes.
- El comportamiento de conexión de TCP siempre implicó espera: las retransmisiones SYN y el backoff exponencial pueden convertir un “host caído” en decenas de segundos sin un timeout a nivel de aplicación.
- Los timeouts de DNS son anteriores a los contenedores: los reintentos del resolvedor entre servidores de nombres pueden exceder la paciencia de tu aplicación, especialmente con dominios de búsqueda rotos.
- La red de Docker evolucionó rápido al principio: el cambio de enlaces legados a bridges definidos por el usuario mejoró DNS/descubrimiento de servicios pero introdujo nuevas capas donde la latencia puede esconderse.
- Los microservicios multiplicaron las fronteras de timeout: una sola solicitud de usuario puede atravesar 5–30 saltos, cada uno con su propio plazo por defecto.
- Las librerías de reintentos se popularizaron tras grandes outages: redujeron el impacto de errores transitorios, pero también habilitaron “tormentas de reintentos” cuando se usan mal.
- Los service meshes normalizaron reintentos y timeouts: los meshes basados en Envoy hicieron policies configurables, y también convirtieron “quién hizo timeout?” en un nuevo juego.
- HTTP/2 cambió la economía de conexiones: menos conexiones, más multiplexación; una única conexión sobrecargada puede amplificar la latencia si el control de flujo está mal afinado.
- Los load balancers en la nube estandarizaron timeouts de inactividad: muchos entornos aún usan por defecto alrededor de un minuto para conexiones inactivas, lo que choca con long polls y streaming.
- Los reinicios de contenedores se volvieron un “arreglo” automático: los orquestadores reinician elementos no saludables; sin un buen ajuste de timeouts, obtienes churn en lugar de recuperación.
El tema: las pilas modernas añadieron más lugares donde “esperar” es una elección de política. Tu trabajo es hacer esas políticas consistentes, acotadas y alineadas con las expectativas del usuario.
Mapea el timeout: dónde ocurre en sistemas Docker
Cuando alguien dice “el contenedor hizo timeout”, pregunta: ¿qué contenedor, en qué dirección, qué protocolo y en qué capa?
Los timeouts se agrupan en unas pocas categorías reales.
1) Descargas de imágenes y acceso al registro
Síntomas: despliegues que se cuelgan, nodos que no arrancan cargas de trabajo, trabajos de CI detenidos en docker pull.
Causas: alcanzabilidad del registro, DNS, bloqueos en el handshake TLS, interferencia de proxy o pérdida de paquetes en la ruta.
2) Llamadas este-oeste (contenedor a contenedor)
Síntomas: 504 intermitentes, “context deadline exceeded” o timeouts en el cliente.
Causas: upstream sobrecargado, presión de conntrack, fallos de DNS, problemas en la red overlay, mismatch de MTU o vecinos ruidosos.
3) Norte-sur (ingress a servicio)
Síntomas: 504/499 del balanceador, timeouts de proxy, subidas colgadas, caídas en long-poll.
Causas: timeouts de inactividad desalineados, buffering del proxy, backends lentos o respuestas grandes sobre enlaces restringidos.
4) Healthchecks, probes y decisiones del orquestador
Síntomas: contenedores que reinician “aleatoriamente”, pero los logs muestran que el servicio estaba bien.
Causas: timeout de healthcheck demasiado corto, inicio no contabilizado, lentitud de dependencias, throttling de CPU, demoras de DNS.
5) Timeouts de apagado
Síntomas: procesos “killed”, estado corrupto, archivos parcialmente escritos, drenado atascado.
Causas: StopTimeout demasiado corto, SIGTERM no manejado, pausas largas de GC, I/O bloqueante, NFS atascado o flush lento a disco.
Guión de diagnóstico rápido (revisa primero/segundo/tercero)
Cuando estás de guardia, no tienes tiempo para admirar la complejidad. Necesitas una secuencia que encuentre el cuello de botella rápido y reduzca el radio de impacto.
Primero: identifica el límite de timeout y quién está esperando
- ¿Es del lado del cliente (logs de la app) o del proxy (logs de ingress) o del kernel (retransmisiones SYN, DNS)?
- ¿Es timeout de conexión o timeout de lectura? Eso apunta a fallos distintos.
- ¿Es un upstream o muchos? Uno sugiere sobrecarga localizada; muchos sugieren infraestructura compartida (DNS, red, nodo).
Segundo: confirma si es capacidad, latencia o fallo de dependencia
- Capacidad: throttling de CPU, hilos agotados, saturación de pools de conexión.
- Latencia: esperas de I/O en disco, retransmisiones de red, DNS lento.
- Fallo de dependencia: errores aguas arriba enmascarados como timeouts por reintentos o logs pobres.
Tercero: busca amplificación por reintentos
- ¿Un 1% de fallos upstream está causando 10x en tasa de solicitudes por reintentos?
- ¿Están múltiples capas reintentando (cliente + sidecar + gateway)?
- ¿Los reintentos se sincronizan (sin jitter), creando ondas de tráfico?
Cuarto: detén la hemorragia de forma segura
- Reduce concurrencia (rate limit, shedding de carga).
- Desactiva reintentos inseguros (operaciones no idempotentes).
- Aumenta timeouts solo cuando puedas probar que el trabajo terminará y la cola no explotará.
Tareas prácticas: comandos, salidas y decisiones (12+)
Estas son tareas “Necesito respuestas ya”. Cada una incluye un comando realista, una salida de ejemplo, qué significa y la decisión que tomas.
Ejecútalas en el host Docker salvo indicación.
Task 1: Check Docker engine health and runtime errors
cr0x@server:~$ sudo systemctl status docker --no-pager
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled)
Active: active (running) since Fri 2026-01-02 09:11:07 UTC; 3h 22min ago
Docs: https://docs.docker.com
Main PID: 1532 (dockerd)
Tasks: 37
Memory: 412.5M
CPU: 19min 12.320s
CGroup: /system.slice/docker.service
└─1532 /usr/bin/dockerd -H fd://
Jan 03 11:58:21 server dockerd[1532]: time="2026-01-03T11:58:21Z" level=warning msg="... i/o timeout"
Significado: Dockerd está corriendo, pero advertencias como “i/o timeout” indican problemas de almacenamiento o red que afectan pulls/logging.
Decisión: Si ves timeouts repetidos aquí, trátalo como problema a nivel de host, no de la app. Continúa con chequeos de red/almacenamiento.
Task 2: Identify which containers are restarting and why
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.RestartCount}}'
NAMES STATUS RESTARTCOUNT
api-7f3c Up 2 minutes (healthy) 4
worker-2a91 Restarting (1) 10 seconds ago 19
nginx-edge Up 3 hours 0
Significado: RestartCount te dice si los timeouts están causando churn. “Restarting” apunta a crash loops o healthchecks fallando.
Decisión: Prioriza el mayor contador de reinicios primero; el churn puede amplificar timeouts al quitar capacidad.
Task 3: Inspect restart policy and stop timeout (graceful shutdown budget)
cr0x@server:~$ docker inspect -f '{{.Name}} Restart={{.HostConfig.RestartPolicy.Name}} StopTimeout={{.HostConfig.StopTimeout}}' worker-2a91
/worker-2a91 Restart=always StopTimeout=10
Significado: StopTimeout=10 significa que Docker envía SIGTERM y luego SIGKILL tras 10 segundos.
Decisión: Si la app necesita más tiempo para drenar o flushear, aumenta StopTimeout y arregla el manejo de SIGTERM; no “reintentes más fuerte.”
Task 4: Inspect healthcheck timing and failures
cr0x@server:~$ docker inspect -f 'HC={{json .Config.Healthcheck}}' api-7f3c
HC={"Test":["CMD-SHELL","curl -fsS http://127.0.0.1:8080/health || exit 1"],"Interval":30000000000,"Timeout":2000000000,"StartPeriod":0,"Retries":3}
Significado: Timeout de healthcheck es 2s, intervalo 30s, retries 3. StartPeriod=0 significa sin gracia de arranque.
Decisión: Si el arranque puede tomar legítimamente 20–60s, añade StartPeriod o un timeout mayor; de lo contrario estás enseñando al orquestador a entrar en pánico.
Task 5: Confirm whether the timeout is connect vs read
cr0x@server:~$ docker logs --tail 30 api-7f3c
2026-01-03T11:57:12Z ERROR upstream_call failed err="dial tcp 10.42.3.18:9090: i/o timeout"
2026-01-03T11:57:44Z ERROR upstream_call failed err="context deadline exceeded (Client.Timeout exceeded while awaiting headers)"
Significado: “dial tcp … i/o timeout” suele ser camino de conexión (alcanzabilidad de red, SYN/ACK, firewall, MTU, conntrack).
“awaiting headers” sugiere que la conexión se estableció pero el servidor no respondió lo bastante rápido (sobrecarga, contención de locks, I/O lento).
Decisión: Divide la investigación: timeouts de conexión → red; timeouts de headers/lectura → saturación o latencia del upstream.
Task 6: Measure DNS latency and failures inside the container
cr0x@server:~$ docker exec -it api-7f3c sh -lc 'time getent hosts redis.default.svc'
10.42.2.9 redis.default.svc
real 0m0.412s
user 0m0.000s
sys 0m0.003s
Significado: 412ms para una búsqueda simple es sospechoso en una LAN rápida. Si a veces salta a segundos, DNS es un sospechoso principal.
Decisión: Si DNS es lento, no aumentes primero los timeouts de la aplicación; arregla la ruta del resolvedor, el caching, los dominios de búsqueda o la carga del servidor DNS.
Task 7: Check container DNS configuration (search domains can be a stealth tax)
cr0x@server:~$ docker exec -it api-7f3c cat /etc/resolv.conf
nameserver 127.0.0.11
options ndots:0
Significado: El DNS embebido de Docker (127.0.0.11) está en juego. Las opciones importan; ndots/search pueden causar múltiples búsquedas por nombre.
Decisión: Si ves listas de búsqueda largas o ndots alto, acórtalos. Menos consultas inútiles = menos timeouts.
Task 8: Verify basic network reachability from the container to upstream
cr0x@server:~$ docker exec -it api-7f3c sh -lc 'nc -vz -w 2 10.42.3.18 9090'
10.42.3.18 (10.42.3.18:9090) open
Significado: El puerto es alcanzable en 2 segundos ahora mismo. Si esto falla intermitentemente, miras rutas fluctuantes, sobrecarga o drops de conntrack.
Decisión: Si siempre funciona pero tu app hace timeout esperando headers, enfócate en rendimiento upstream, no en ACLs de red.
Task 9: Check for packet loss/retransmits on the host (timeouts love packet loss)
cr0x@server:~$ sudo netstat -s | egrep -i 'retransmit|timeout|listen'
18342 segments retransmitted
27 TCP timeouts in loss recovery
Significado: Retransmisiones y timeouts de recuperación indican problemas de calidad de red o congestión.
Decisión: Si las retransmisiones suben durante incidentes, no “arregles” aumentando timeouts de aplicación. Arregla la pérdida: mismatch de MTU, NIC defectuosa, ruta congestionada, vecino ruidoso o host sobrecargado.
Task 10: Inspect conntrack usage (classic cause of weird connect timeouts)
cr0x@server:~$ sudo sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_count = 262041
net.netfilter.nf_conntrack_max = 262144
Significado: Básicamente estás en el límite de conntrack. Cuando está lleno, nuevas conexiones se descartan o se comportan erráticamente, a menudo como timeouts.
Decisión: Reduce churn de conexiones (keepalive, pooling), aumenta netamente conntrack max con cuidado y evita reintentos que creen tormentas de conexiones.
Task 11: Look for CPU throttling (timeouts that aren’t “slow,” just “not scheduled”)
cr0x@server:~$ docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}'
NAME CPU % MEM USAGE / LIMIT NET I/O
api-7f3c 285.12% 612MiB / 2GiB 1.2GB / 1.1GB
worker-2a91 98.44% 1.7GiB / 2GiB 88MB / 91MB
Significado: CPU alta puede manifestarse como timeouts porque el trabajo se encola detrás de CPU.
Decisión: Si la CPU está saturada, los reintentos lo empeorarán. Reduce carga, limita concurrencia y escala u optimiza.
Task 12: Identify disk I/O stalls on the host (the silent timeout generator)
cr0x@server:~$ iostat -x 1 3
Linux 6.5.0 (server) 01/03/2026 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.20 0.00 4.10 18.40 0.00 65.30
Device r/s w/s rkB/s wkB/s await svctm %util
nvme0n1 120.0 980.0 5200.0 64200.0 48.2 0.9 92.0
Significado: Alto iowait y await elevado (48ms) sugieren disco saturado. Contenedores esperando I/O pueden parecer timeouts de red a los clientes.
Decisión: Si los picos de await correlacionan con timeouts, arregla I/O: mueve rutas calientes a almacenamiento más rápido, reduce escrituras sincrónicas, ajusta logging o aísla vecinos ruidosos.
Task 13: Check container log driver and log pressure
cr0x@server:~$ docker info --format 'LoggingDriver={{.LoggingDriver}}'
LoggingDriver=json-file
Significado: Logging json-file puede convertirse en problema de I/O si los logs son enormes y la rotación está mal configurada.
Decisión: Si el disco está caliente y los logs son ruidosos, limita el tamaño de logs y rota; no lo soluciones con timeouts más largos.
Task 14: Inspect Docker daemon events around the incident window
cr0x@server:~$ docker events --since 30m --until 0m
2026-01-03T11:41:02.112345678Z container die api-7f3c (exitCode=137)
2026-01-03T11:41:02.223456789Z container start api-7f3c (image=myrepo/api:latest)
2026-01-03T11:41:33.334567890Z container health_status: unhealthy api-7f3c
2026-01-03T11:42:03.445678901Z container health_status: healthy api-7f3c
Significado: exitCode=137 insinúa SIGKILL (a menudo StopTimeout excedido u OOM killer). Los flaps de health muestran umbrales de timeout al límite.
Decisión: Si ves SIGKILL, arregla el apagado. Si ves flaps de health, ajusta el timing del healthcheck e investiga presión de recursos.
Task 15: Confirm stop behavior (is the app honoring SIGTERM?)
cr0x@server:~$ docker stop -t 10 api-7f3c
api-7f3c
Significado: Esto solicita un stop gracioso de 10s. Si rutinariamente tarda más o es matado, la app no está drenando rápido.
Decisión: Implementa manejo de SIGTERM, deja de aceptar nuevas solicitudes, drena conexiones y luego sal. Aumenta StopTimeout solo después de haberlo ganado.
Task 16: Test registry pull latency explicitly (separate pull timeout from runtime timeouts)
cr0x@server:~$ time docker pull alpine:3.19
3.19: Pulling from library/alpine
Digest: sha256:4b1d...
Status: Image is up to date for alpine:3.19
real 0m1.208s
user 0m0.074s
sys 0m0.062s
Significado: El pull es rápido ahora. Si los despliegues fallan solo en horas pico, el registro o proxy está limitando o tu NAT está estresado.
Decisión: Si los pulls son lentos, añade cache (mirror de registro) o arregla egress; no “reintentes para siempre” durante despliegues.
Ajusta los reintentos correctamente: presupuestos, backoff y jitter
Los reintentos no son gratuitos. Consumen capacidad, aumentan la latencia cola y convierten pequeñas tasas de fallo en grandes incrementos de tráfico.
Sin embargo, también son una de las mejores herramientas cuando están acotados y selectivos.
La mentalidad del presupuesto de reintentos
Empieza con una regla simple: Los reintentos deben caber dentro del plazo del usuario. Si una solicitud tiene un presupuesto SLO de 2s, no puedes
hacer tres intentos de 2s. Eso no es resiliencia; es engañarte con matemáticas.
El “presupuesto” debe considerar:
- Tiempo de conexión
- Tiempo de procesamiento del servidor
- Tiempo de encolamiento en el cliente (pools de hilos, ejecutores async)
- Delay de backoff entre intentos
- La peor variación de la red
¿Qué fallos son reintentables?
Reintenta solo cuando la falla sea plausiblemente transitoria y la operación sea segura.
- Buenos candidatos: reset de conexión, 503 temporal del upstream, algunos casos 429 (si respetas Retry-After), DNS SERVFAIL (quizá), GETs idempotentes.
- Malos candidatos: timeouts con estado del servidor desconocido en peticiones no idempotentes (POST que pudo haberse completado), 4xx deterministas, fallos de auth, “payload too large”.
- Complicados: timeouts de lectura—a veces el upstream está lento, a veces está muerto. Reintentar puede duplicar la carga en un servicio que lucha.
Backoff y jitter: evita tormentas sincronizadas de reintentos
Si cada cliente reintenta exactamente a los 100ms, obtienes un thundering herd: picos periódicos de tráfico que mantienen el sistema en un estado cercano al fallo.
Usa backoff exponencial con jitter. Sí, el jitter parece superstición. No lo es; es probabilidad aplicada.
Una política por defecto sensata para muchos RPC internos:
- Intentos máximos: 2–3 (incluyendo el primer intento)
- Backoff: exponencial empezando en 50–100ms
- Jitter: full jitter o equal jitter
- Timeout por intento: menor que el plazo global (por ejemplo, 300ms por intento dentro de 1s de presupuesto)
No apiles reintentos en varias capas
Si tu aplicación reintenta, tu sidecar reintenta y tu gateway reintenta, construiste una máquina tragaperras. Mayormente pierde, pero está muy confiada.
Elige una capa para encargarse de reintentos en una ruta de llamada dada. Haz que las demás observen e impongan deadlines, no que amplifiquen tráfico.
Por qué “simplemente aumentar el timeout” a menudo falla
Aumentar timeouts puede ayudar cuando tienes operaciones raras y acotadas que terminarán si esperas un poco más.
Falla cuando:
- Las solicitudes se encolan detrás de sobrecarga: timeouts más largos solo permiten colas más profundas.
- Las dependencias están caídas: solo estás ocupando hilos y sockets por más tiempo.
- Estás enmascarando un agujero negro de red (MTU, firewall): esperar más no cambia la física.
Chiste corto #2: Un timeout es una fecha límite, no un estilo de vida.
Un patrón de ajuste concreto que funciona
Para un cliente HTTP llamando a un upstream dentro del mismo cluster/VPC:
- Timeout global de solicitud: 800ms–2s (según SLO)
- Timeout de conexión: 50ms–200ms (fallo rápido en inalcanzable)
- Timeout por intento: 300ms–800ms
- Reintentos: 1 reintento para solicitudes idempotentes (2 intentos totales)
- Backoff: 50ms → 150ms con jitter
- Tope duro en solicitudes concurrentes en vuelo (bulkhead)
El bulkhead no es opcional. Reintentos sin límites de concurrencia son cómo te auto-DDoS a tu propio upstream.
Inicio, healthchecks y apagado: timeouts que controlas
La mayoría de los “timeouts de contenedores” que despiertan a la gente por la noche son autoinfligidos por mala configuración de ciclo de vida: la app necesita tiempo para arrancar,
la plataforma espera que sea instantánea y luego todos discuten con un gráfico.
Arranque: da un periodo de gracia, no una correa más larga
Si tu servicio carga modelos, precalienta caches, ejecuta migraciones o espera una dependencia, a veces será lento.
Tu trabajo es separar “arrancando” de “no saludable”.
- Usa un periodo de gracia de inicio (StartPeriod de healthcheck de Docker, o probes de arranque del orquestador).
- Haz endpoints de salud baratos y conscientes de dependencias: “estoy vivo?” difiere de “¿puedo servir tráfico?”
- No ejecutes migraciones de esquema en cada réplica al iniciar. Eso no es “automatizado”; es “dolor sincronizado”.
Healthchecks: timeouts pequeños, pero realistas
Un timeout de healthcheck de 1–2 segundos está bien para la mayoría de endpoints locales—si el contenedor tiene CPU y no está bloqueado en disco.
Pero si lo pones en 200ms porque “rápido es mejor”, no mejoras la confiabilidad. Aumentas la probabilidad de reinicios bajo la jitter normal.
Apagado: trátalo como una ruta de primer orden
Docker envía SIGTERM, espera y luego envía SIGKILL. Si tu app ignora SIGTERM o bloquea mientras vacía logs a un disco lento, será matada.
Los procesos matados pierden solicitudes, corrompen estado y desencadenan reintentos desde clientes—que parecen timeouts.
Guía práctica:
- Maneja SIGTERM: deja de aceptar trabajo nuevo, drena y luego sal.
- Configura StopTimeout para cubrir el peor caso de drenado, pero mantenlo acotado.
- Prefiere timeouts de keepalive y de solicitud más cortos para que el drenado termine rápido.
- Haz observable la “latencia de apagado”: registra inicio/fin de terminación y número de solicitudes en vuelo.
Proxies, balanceadores y cadenas de múltiples timeouts
Una solicitud de usuario a menudo cruza múltiples timeouts:
navegador → CDN → balanceador → proxy de entrada → service mesh → app → base de datos.
Si esos plazos no están alineados, gana el más corto—y puede no ser el que esperas.
Cómo los timeouts desajustados crean fallos fantasma
Patrón de ejemplo:
- Timeout del cliente: 10s
- Timeout del proxy de ingreso: 5s
- Timeout de la app al DB: 8s
A los 5 segundos, el ingreso se rinde y cierra la conexión del cliente. La app sigue trabajando hasta los 8 segundos y luego cancela la llamada a DB.
Mientras tanto, la DB puede seguir procesando. Has convertido una solicitud lenta en trabajo desperdiciado en tres capas.
Cómo se ve una buena alineación
- Las capas externas tienen timeouts ligeramente mayores que las internas, pero no dramáticamente mayores.
- Cada salto aplica un deadline y lo propaga río abajo (headers, propagación de contexto).
- Los reintentos ocurren en una capa, con conciencia de idempotencia y presupuesto.
Ten cuidado con los timeouts de inactividad
Los timeouts de inactividad matan conexiones “silenciosas”. Eso importa para:
- Server-sent events
- WebSockets
- Long-poll
- Grandes subidas donde el cliente pausa
Si tienes streaming, ajusta explícitamente los timeouts de inactividad y añade heartbeats a nivel de aplicación para que “inactivo” no se confunda con “muerto.”
Almacenamiento y latencia I/O: la causa que nadie quiere
A los ingenieros les gusta depurar redes porque las herramientas se ven bien y los gráficos son nítidos. La latencia de almacenamiento es menos glamorosa: aparece como “await”
y entristece a todos.
Los contenedores hacen timeout porque esperan I/O más a menudo de lo que los equipos admiten. Villanos comunes:
- Volumen de logs escribiendo demasiado en discos lentos
- Overhead del sistema de ficheros overlay con muchos writes pequeños
- Hiccup en almacenamiento en red (NFS atascos, congestión iSCSI)
- Bases de datos con sync intensivo en nodos saturados
- Throttling de disco a nivel de nodo en entornos virtualizados
Cómo la latencia de almacenamiento se vuelve “timeout de red”
Tu handler de API escribe una línea de log, hace flush de un buffer o escribe en un directorio cache local.
Esa escritura bloquea 200ms–2s porque el disco está ocupado. El hilo del handler no puede responder.
El cliente ve “awaiting headers” y lo llama timeout de red.
Lo arreglas:
- Reduciendo escrituras síncronas en rutas de petición
- Usando logging en buffer/async
- Poniendo cargas stateful en la clase de almacenamiento correcta
- Separando cargas ruidosas de las sensibles a latencia
Tres mini-historias del mundo corporativo
Mini-historia 1: El incidente causado por una suposición equivocada
Una empresa mediana corría un conjunto de servicios internos en hosts Docker con un proxy inverso simple delante. El proxy tenía un timeout upstream de 5 segundos.
Los equipos de app asumieron que tenían “10 segundos” porque sus clientes HTTP estaban configurados a 10 segundos. Nadie anotó la configuración del proxy porque “es solo infraestructura.”
Durante un mantenimiento rutinario de la base de datos, la latencia de consultas pasó de <100ms a multi-segundos. La aplicación empezó a devolver respuestas alrededor de 6–8 segundos.
Los clientes esperaron pacientemente. El proxy no. Empezó a cortar conexiones a los 5 segundos, devolviendo 504s.
Los logs de la aplicación eran engañosos: las solicitudes parecían “terminar”, pero el cliente ya se cansó. Algunos clientes reintentaron automáticamente. Ahora la misma petición costosa
corría dos, a veces tres veces. La carga en la base de datos subió, la latencia subió y más solicitudes cruzaron el límite de 5 segundos del proxy. Un mantenimiento limpio se convirtió en un incidente real.
Arreglarlo fue vergonzosamente sencillo: alinear deadlines. El timeout del proxy quedó ligeramente mayor que el timeout DB de la app, y el timeout cliente quedó ligeramente mayor que el del proxy.
También deshabilitaron reintentos para endpoints no idempotentes y añadieron claves de idempotencia donde hacía falta. Lo mejor: la siguiente ventana de mantenimiento fue aburrida, que es el resultado correcto.
Mini-historia 2: La “optimización” que salió mal
Otra organización quiso failover más rápido. Alguien redujo timeouts de llamadas de servicio de 2s a 200ms e incrementó reintentos de 1 a 5 “para compensar.”
Parecía astuto: detección rápida, múltiples intentos, menos errores visibles. En staging incluso parecía funcionar—staging rara vez tiene encolamiento o congestión real.
En producción, un servicio downstream tenía picos de latencia ocasionales de 300–600ms durante GC y refrescos de cache periódicos. Con la nueva configuración, la varianza normal se volvió fallo.
Los clientes alcanzaban 200ms, hacían timeout, reintentaban, volvían a hacer timeout y repetían. No fallaban más rápido; fallaban más ruidosamente.
El servicio downstream no solo vio más tráfico; vio tráfico sincronizado en ráfagas. Cinco reintentos sin jitter significaron olas de carga cada pocos cientos de milisegundos.
La CPU subió. La latencia de cola empeoró. El downstream se quedó atrás y empezó a hacer timeouts reales. Ahora los upstreams también quemaban CPU en reintentos y saturaban pools de conexión.
Revertir la “optimización” estabilizó el sistema en minutos. La solución duradera fue más madura: un solo reintento con backoff exponencial y jitter,
un timeout por intento emparejado al p95 observado, y un tope global de concurrencia. También añadieron dashboards que graficaban tasa de reintentos y la amplificación de QPS efectiva.
La lección oculta: trabajo de resiliencia que ignora las colas de la distribución es solo optimismo con YAML.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día
Un equipo de servicios financieros ejecutaba workers batch dockerizados que hablaban con una API externa. La API ocasionalmente limitaba con 429s.
El equipo tenía una política aburrida: respetar Retry-After, limitar reintentos a 2 y aplicar un deadline duro por trabajo. Nada heroico, nada infinito.
Una tarde, el proveedor externo tuvo una caída parcial. Muchos clientes vieron fallos en cascada porque sus clientes reintentaron agresivamente y bombardearon la API ya debilitada.
Los workers de este equipo frenaron en lugar de acelerarse. Los jobs tardaron más, pero el sistema se mantuvo estable.
Sus dashboards mostraron 429s elevados y mayor duración de jobs, pero la cola no explotó. ¿Por qué? Presupuesto de reintentos más contraflujo.
También tenían un circuito abierto: tras un umbral de fallos, los workers dejaron de llamar a la API durante una ventana de enfriamiento.
Cuando el proveedor recuperó, la cola del equipo se vació de manera predecible. Sin escalado de emergencia, sin timeouts misteriosos, sin debates sobre “debemos añadir más reintentos.”
Las prácticas aburridas no llenan conferencias. Te mantienen fuera de puentes de incidentes, que es mejor.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: los timeouts suben justo cuando un upstream se vuelve más lento
Causa raíz: los reintentos amplifican la carga; múltiples capas reintentando; sin backoff/jitter.
Solución: reduce intentos (a menudo a 2 totales), añade backoff exponencial con jitter, impone límites de concurrencia y asegura que solo una capa reintente.
2) Síntoma: “dial tcp … i/o timeout” en muchos servicios
Causa raíz: tabla conntrack llena, pérdida de paquetes, mismatch de MTU o firewall que descarta SYN/ACK.
Solución: revisa utilización de conntrack, reduce churn de conexiones vía keepalive/pooling, aumenta conntrack max con cuidado y valida MTU de extremo a extremo.
3) Síntoma: contenedores reinician y los logs muestran que “estaban bien”
Causa raíz: timeout de healthcheck demasiado agresivo; falta gracia de arranque; checks de dependencias en liveness.
Solución: separa lógica de liveness vs readiness, añade StartPeriod (o probe de arranque), ajusta timeout para que refleje el p95 real del endpoint de salud bajo carga.
4) Síntoma: las solicitudes hacen timeout durante despliegues, no en estado estable
Causa raíz: apagado no gracioso; StopTimeout demasiado corto; drenado no implementado; el balanceador sigue enviando a tareas terminándose.
Solución: implementa drenado con SIGTERM, aumenta StopTimeout apropiadamente, configura deregistro/delay de drain en el LB y reduce timeouts de keepalive/solicitud para salir más rápido.
5) Síntoma: los timeouts “awaiting headers” empeoran cuando el logging es verboso
Causa raíz: saturación de I/O en disco por volumen de logs o driver json-file; overhead del filesystem overlay.
Solución: limita/rota logs, envía logs de forma asíncrona, mueve discos calientes a SSD/NVMe, aísla cargas stateful y mide iostat await durante incidentes.
6) Síntoma: búsquedas DNS a veces tardan segundos
Causa raíz: reintentos del resolvedor, DNS sobrecargado, mala configuración de dominios de búsqueda, cuello de botella de DNS embebido o pérdida de paquetes intermitente.
Solución: mide latencia de lookup dentro de contenedores, simplifica dominios de búsqueda/opciones, añade caching y asegúrate de que los servidores DNS tengan capacidad y poca pérdida.
7) Síntoma: conexiones de larga duración se caen alrededor del mismo intervalo
Causa raíz: timeouts de inactividad en LB/proxy; falta de keepalives o heartbeats.
Solución: alinea timeouts de inactividad entre capas y añade heartbeats de aplicación para streaming/WebSockets.
8) Síntoma: aumentar timeouts empeora todo pero más lento
Causa raíz: estás sobrecargado; timeouts más largos profundizan colas y aumentan holding de recursos.
Solución: shed load, reduce concurrencia, escala capacidad y acorta timeouts internos para que los fallos liberen recursos rápido.
Listas de verificación / plan paso a paso
Paso a paso: arregla timeouts de contenedores sin reintentos infinitos
- Clasifica el timeout: conexión vs lectura vs inactividad vs apagado. Usa logs y métricas de proxy para identificar dónde se dispara.
- Encuentra el deadline más corto en la cadena: CDN/LB/ingress/mesh/app/db. El timeout más pequeño gobierna la experiencia del usuario.
- Mide latencia p50/p95/p99 para la ruta de llamada. Ajusta por las colas altas, no por la mediana.
- Fija un deadline global por solicitud basado en SLO y UX (lo que el usuario tolera).
- Divide los timeouts: timeout de conexión corto, timeout de lectura acotado y un deadline global.
- Elige un único propietario de reintentos (librería cliente o mesh, no ambos). Desactiva reintentos en otros sitios.
- Limita reintentos: típicamente 1 reintento para llamadas idempotentes. Más intentos requieren evidencia y mayor presupuesto.
- Añade backoff exponencial con jitter, siempre. Sin excepciones para tráfico “interno”.
- Añade bulkheads: limita concurrencia y longitud de cola; preferir fallar rápido a dejar que las colas crezcan.
- Haz operaciones inseguras seguras: claves de idempotencia para POST/PUT cuando la lógica de negocio lo permita.
- Arregla timeouts de ciclo de vida: StartPeriod de healthcheck, timeout realista de health y StopTimeout alineado con el drenado.
- Valida restricciones del host: reserva en conntrack, pérdida de paquetes, throttling de CPU, await de disco.
- Prueba que el cambio funcionó: observa tasa de reintentos, amplificación de QPS upstream, latencia p99 y consumo del presupuesto de errores.
Checklist rápido: qué cambiar primero (mayor ROI)
- Elimina reintentos infinitos. Reemplázalos por intentos máximos y deadline.
- Añade jitter a cualquier bucle de reintento/backoff.
- Asegura que solo una capa reintente.
- Arregla StartPeriod de healthcheck y timeouts excesivamente cortos.
- Revisa utilización de conntrack y await de disco durante incidentes.
Preguntas frecuentes
1) ¿Debería usar alguna vez reintentos infinitos?
Casi nunca. Los reintentos infinitos pertenecen solo a sistemas de background controlados con backpressure explícito, colas durables y comportamiento visible de dead-letter.
Para solicitudes orientadas al usuario, los reintentos infinitos convierten outages en desastres en cámara lenta.
2) ¿Cuántos reintentos son “seguros” para llamadas internas?
Comúnmente: un reintento para operaciones idempotentes, con backoff y jitter, y solo si tienes capacidad sobrante. Si el upstream está sobrecargado, los reintentos no son “seguros”, son gasolina.
3) ¿Cuál es la diferencia entre connect timeout y read timeout?
El connect timeout cubre el establecimiento de la conexión (ruteo, SYN/ACK, handshake TLS). El read timeout cubre esperar la respuesta una vez que la conexión existe.
Los connect timeouts te apuntan a red/conntrack/firewalls. Los read timeouts te apuntan a latencia upstream, encolamiento o stalls de I/O.
4) ¿Por qué los timeouts aumentan durante despliegues?
Porque el apagado y la readiness suelen manejarse mal. Los contenedores en terminación siguen recibiendo tráfico, aceptan solicitudes sin estar listos o reciben SIGKILL a mitad de vuelo.
Arregla el drenado, los stop timeouts y el timing de deregistro del balanceador.
5) ¿Cómo sé si los reintentos están causando una tormenta de reintentos?
Busca un salto en QPS upstream que no coincide con el tráfico de usuario, además de aumento de errores y latencia p99. También revisa si los fallos se agrupan en ondas periódicas (falta de jitter).
Controla “intentos por solicitud” si puedes.
6) ¿Son los timeouts de healthcheck lo mismo que los timeouts de solicitud?
No. Los healthchecks son la plataforma decidiendo si matar o enrutar a un contenedor. Los timeouts de solicitud son los clientes decidiendo si esperar.
Un mal healthcheck puede parecer “timeouts aleatorios” porque quita capacidad o reinicia el servicio en plena carga.
7) ¿Por qué importa tanto el DNS dentro de contenedores?
Porque cada llamada a servicio empieza con una búsqueda de nombre a menos que fijes IPs (no lo hagas). Si DNS es lento, cada solicitud paga ese impuesto, y los reintentos lo multiplican.
El DNS en contenedores añade otra capa (DNS embebido o cache local) que puede convertirse en cuello de botella.
8) ¿Cuándo es correcto aumentar un timeout?
Cuando has probado que el trabajo se completa con un poco más de tiempo y tienes capacidad para mantener recursos ocupados más tiempo.
Ejemplo: un endpoint de generación de reportes conocido como lento pero acotado, con baja concurrencia y colas adecuadas.
9) ¿Cuál es la mejor forma de evitar efectos duplicados al reintentar POSTs?
Usa claves de idempotencia (IDs de solicitud generados por el cliente guardados server-side), o diseña la operación para que sea idempotente.
Si no puedes, no reintentes automáticamente—muestra el fallo y deja que un humano o un sistema de jobs lo reconcilie.
10) ¿Cómo prevengo que los timeouts se propaguen en cascada entre servicios?
Usa deadlines propagadas end-to-end, bulkheads (límites de concurrencia), circuit breakers y reintentos acotados con jitter.
También mantiene timeouts interiores más cortos que los exteriores para que el fallo se detenga antes río abajo.
Conclusión: pasos prácticos siguientes
Los timeouts no son el enemigo. Lo que sí lo es es la espera sin límites. Si quieres menos incidentes, deja de tratar los reintentos como magia y empieza a tratarlos como gasto controlado.
- Elige un deadline de solicitud que refleje la realidad (SLO y tolerancia del usuario).
- Divide timeouts de conexión/lectura y regístralos de forma distinta.
- Limita reintentos (usualmente uno) y añade backoff exponencial con jitter.
- Asegura que solo una capa reintente; el resto debe hacer cumplir deadlines.
- Arregla el tuning de ciclo de vida: StartPeriod de healthcheck, timeout realista de health y apagado gracioso con StopTimeout suficiente.
- Durante el próximo incidente de timeout, ejecuta el guión de diagnóstico rápido y las tareas anteriores—especialmente conntrack, latencia DNS y await de disco.
Si haces solo dos cosas esta semana: elimina reintentos infinitos y alinea timeouts entre proxies y servicios. Notarás la diferencia la próxima vez que la latencia se ponga rara.