Vous regardez un tableau de bord. La latence grimpe. Quelques conteneurs redémarrent. Puis votre canal d’incident se remplit des mêmes deux mots :
« délai d’attente ».
Quelqu’un suggère « augmentez juste les timeouts » et quelqu’un d’autre propose « ajoutez des retries ». La troisième personne — toujours la troisième — suggère « retries infinis ».
Cette dernière transforme une petite défaillance en une panne durable.
Ce que sont vraiment les timeouts (et pourquoi ce ne sont pas des bugs)
Un timeout est une décision. C’est le moment où votre système dit « j’ai fini d’attendre ». Cette décision peut se produire dans une bibliothèque cliente, un reverse proxy,
un service mesh, la pile TCP du noyau, un résolveur DNS ou un plan de contrôle. Parfois dans plusieurs de ces éléments à la fois.
Le piège est de considérer le « timeout » comme un seul réglage. Ce n’est pas le cas. « Timeout » est une famille de délais qui interagissent : timeout de connexion, timeout de lecture,
timeout d’écriture, timeout d’inactivité, timeout de keepalive, timeout de healthcheck, timeout d’arrêt gracieux, etc. Chacun existe pour limiter l’usage des ressources
et contenir la défaillance.
Si vous ajoutez des retries infinis par-dessus, vous supprimez la contention. L’erreur cesse d’être visible, mais la charge ne disparaît pas — elle se déplace dans les files,
les pools de connexions, les piles de threads et les services aval. Vous perdez aussi le signal le plus important pendant un incident : le taux d’échec.
Votre objectif n’est pas « pas de timeouts ». Votre objectif est « des timeouts qui échouent vite pour les requêtes sans espoir, retentent seulement quand c’est sûr,
et arrêtent avant que le système ne s’effondre ».
Blague courte #1 : Les retries infinis, c’est comme crier « ON EST ARRIVÉS ? » pendant un voyage en voiture. Vous n’arrivez pas plus vite ; vous rendez juste tout le monde misérable.
Ce que vous devriez optimiser
- Attente bornée : chaque requête a une échéance. Au-delà, abandonnez et passez à autre chose.
- Retries bornés : une politique de retry est un budget, pas un espoir.
- Backoff et jitter explicites : pour éviter des tempêtes de retries synchronisés.
- Conscience de l’idempotence : on ne peut pas retenter tout en toute sécurité.
- Visibilité des défaillances : les erreurs doivent remonter assez vite pour déclencher des mesures d’atténuation.
Une citation pour rester honnête
Werner Vogels (idée paraphrasée) : « Tout échoue ; concevez pour contenir et récupérer des échecs plutôt que de prétendre qu’ils n’arriveront pas. »
Faits et histoire : comment nous avons autant de timeouts
Les timeouts dans les systèmes conteneurisés n’apparaissent pas parce que les ingénieurs sont devenus moins bons. Ils se sont multipliés parce que les systèmes sont devenus plus distribués,
plus empilés et plus dépendants de réseaux qui se comportent parfois comme des réseaux.
- Le comportement de connexion TCP a toujours impliqué de l’attente : les retransmissions SYN et le backoff exponentiel peuvent transformer un « hôte down » en dizaines de secondes sans timeout applicatif.
- Les timeouts DNS sont antérieurs aux conteneurs : les retries des résolveurs entre serveurs de noms peuvent dépasser la patience de votre application, surtout avec des domaines de recherche cassés.
- Le réseau Docker a rapidement évolué : le passage des liens legacy aux bridges définis par l’utilisateur a amélioré la découverte mais introduit de nouvelles couches où la latence peut se cacher.
- Les microservices ont multiplié les frontières de timeout : une seule requête utilisateur peut traverser 5–30 sauts, chacun avec son propre délai par défaut.
- Les bibliothèques de retry sont devenues populaires après de gros incidents : elles ont réduit l’impact d’erreurs transitoires, mais ont aussi permis des « tempêtes de retries » quand mal utilisées.
- Les service meshes ont normalisé retries et timeouts : les meshes basés sur Envoy ont rendu les politiques configurables, et ont aussi rendu « qui a time-out ? » un nouveau jeu.
- HTTP/2 a changé l’économie des connexions : moins de connexions, plus de multiplexage ; une seule connexion surchargée peut amplifier la latence si le contrôle de flux est mal réglé.
- Les LB cloud ont standardisé des timeouts d’inactivité : beaucoup d’environnements par défaut posent une minute pour les connexions inactives, ce qui entre en conflit avec les longues requêtes et le streaming.
- Les redémarrages de conteneurs sont devenus un « correctif » automatique : les orchestrateurs redémarrent les éléments unhealthy ; sans un bon réglage des timeouts, vous obtenez du churn au lieu de la récupération.
Le thème : les stacks modernes ont ajouté plus d’endroits où « attendre » est un choix de politique. Votre travail est de rendre ces politiques cohérentes, bornées et alignées sur les attentes utilisateurs.
Cartographier le timeout : où il survient dans les systèmes Docker
Quand quelqu’un dit « le conteneur a timeout », demandez : quel conteneur, dans quelle direction, quel protocole et quelle couche ?
Les timeouts se regroupent en quelques catégories concrètes.
1) Pulls d’images et accès au registre
Symptômes : déploiements bloqués, nœuds incapables de lancer des workloads, jobs CI bloqués sur docker pull.
Causes : atteignabilité du registre, DNS, blocages TLS, interférence de proxy, ou perte de paquets sur le chemin.
2) Appels est-ouest (container à container)
Symptômes : 504 intermittents, « context deadline exceeded », ou timeouts côté client.
Causes : upstream surchargé, pression conntrack, basculements DNS, problèmes d’overlay network, mismatch MTU, ou voisins bruyants.
3) Nord-sud (ingress vers service)
Symptômes : 504/499 du load balancer, timeouts de proxy, uploads bloqués, abandons de long-poll.
Causes : timeouts d’inactivité mal assortis, buffering proxy, backends lents, ou grosses réponses sur liens contraints.
4) Healthchecks, probes et décisions de l’orchestrateur
Symptômes : conteneurs qui redémarrent « aléatoirement », mais les logs montrent que le service allait bien.
Causes : timeout de healthcheck trop court, démarrage non pris en compte, lenteur d’une dépendance, throttling CPU, délais DNS.
5) Timeouts d’arrêt
Symptômes : processus « killed », état corrompu, fichiers partiellement écrits, drain bloqué.
Causes : StopTimeout trop court, SIGTERM non géré, pauses GC longues, I/O bloquante, NFS coincé, ou flush disque lent.
Playbook de diagnostic rapide (vérifier premier/deuxième/troisième)
Quand vous êtes en astreinte, vous n’avez pas le temps d’admirer la complexité. Vous avez besoin d’une séquence qui trouve vite le goulet et réduit le périmètre.
Premier : identifier la frontière du timeout et qui attend
- Est-ce côté client (logs applicatifs), côté proxy (logs d’ingress) ou côté noyau (retransmissions SYN, DNS) ?
- Est-ce un timeout de connexion ou de lecture ? Ces cas pointent vers des défaillances différentes.
- Est-ce un upstream unique ou plusieurs ? Un seul indique une surcharge locale ; plusieurs indiquent une infra partagée (DNS, réseau, nœud).
Deuxième : confirmer si c’est capacité, latence ou défaillance d’une dépendance
- Capacité : throttling CPU, threads épuisés, saturation des pools de connexions.
- Latence : attentes I/O disque, retransmissions réseau, DNS lent.
- Défaillance de dépendance : erreurs upstream masquées en timeouts par les retries ou un logging insuffisant.
Troisième : vérifier l’amplification par les retries
- Un échec upstream à 1% provoque-t-il un taux de requêtes 10× à cause des retries ?
- Plusieurs couches retentent-elles (client + sidecar + gateway) ?
- Les retries sont-ils alignés et synchrones (sans jitter), créant des vagues de trafic ?
Quatrième : arrêter l’hémorragie en toute sécurité
- Réduisez la concurrence (rate limit, shed load).
- Désactivez les retries dangereux (opérations non idempotentes).
- Augmentez les timeouts seulement si vous pouvez prouver que le travail finira et que la file ne va pas exploser.
Tâches pratiques : commandes, sorties et décisions (12+)
Ce sont des tâches « j’ai besoin de réponses maintenant ». Chacune inclut une commande réaliste, une sortie d’exemple, ce que cela signifie, et la décision à prendre.
Exécutez-les sur l’hôte Docker sauf indication contraire.
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"
Signification : Dockerd fonctionne, mais des avertissements comme « i/o timeout » laissent penser à des problèmes de stockage ou de réseau affectant les pulls/le logging.
Décision : Si vous voyez des timeouts récurrents ici, traitez-le comme un problème au niveau de l’hôte, pas une erreur applicative. Passez aux vérifications réseau/stockage.
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
Signification : RestartCount vous indique si les timeouts provoquent du churn. « Restarting » pointe vers des crash loops ou des healthchecks qui échouent.
Décision : Concentrez-vous d’abord sur le plus grand nombre de redémarrages ; le churn peut amplifier les timeouts en supprimant de la capacité.
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
Signification : StopTimeout=10 signifie que Docker envoie SIGTERM puis SIGKILL après 10 secondes.
Décision : Si l’application a besoin de plus de temps pour drainer ou vider ses flux, augmentez StopTimeout et corrigez la gestion de SIGTERM ; ne « retryez » pas plus fort.
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}
Signification : Timeout du healthcheck 2s, intervalle 30s, retries 3. StartPeriod=0 signifie aucune grâce au démarrage.
Décision : Si le démarrage peut prendre légitimement 20–60s, ajoutez StartPeriod ou augmentez le timeout ; sinon vous entraînez l’orchestrateur à paniquer.
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)"
Signification : « dial tcp … i/o timeout » est généralement le chemin de connexion (atteignabilité réseau, SYN/ACK, firewall, MTU, conntrack).
« awaiting headers » suggère que la connexion a réussi mais que le serveur n’a pas répondu assez vite (surcharge, contention, I/O lente).
Décision : Scindez l’investigation : timeouts de connexion → réseau ; timeouts d’en-tête/lecture → saturation ou latence 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
Signification : 412ms pour une simple résolution est suspect sur un LAN rapide. Si cela monte parfois à des secondes, le DNS est un suspect majeur.
Décision : Si le DNS est lent, n’augmentez pas d’abord les timeouts applicatifs ; corrigez le chemin du résolveur, la mise en cache, les domaines de recherche ou la charge du serveur 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
Signification : Le DNS embarqué de Docker (127.0.0.11) est en jeu. Les options comptent ; ndots/domaines de recherche peuvent provoquer plusieurs requêtes par nom.
Décision : Si vous voyez de longues listes de recherche ou un ndots élevé, resserrez-les. Moins de requêtes inutiles = moins de 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
Signification : Le port est joignable en 2 secondes pour l’instant. Si cela échoue de façon intermittente, vous regardez des routes qui flapent, de la surcharge ou des drops conntrack.
Décision : Si cela réussit de façon consistante mais que votre app timeoute en attendant les en-têtes, concentrez-vous sur la performance upstream, pas sur les ACL réseau.
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
Signification : Les retransmissions et timeouts en récupération de perte indiquent des problèmes de qualité réseau ou de congestion.
Décision : Si les retransmissions augmentent pendant les incidents, n’« améliorez » pas en augmentant les timeouts applicatifs. Corrigez la perte : mismatch MTU, NIC défaillant, chemin congestionné, voisin bruyant ou hôte surchargé.
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
Signification : Vous êtes presque à la limite du conntrack. Quand il est plein, les nouvelles connexions sont abandonnées ou se comportent de façon erratique, souvent sous forme de timeouts.
Décision : Réduisez le churn de connexion (keepalive, pooling), augmentez le max conntrack avec prudence, et arrêtez les retries qui créent des tempêtes de connexions.
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
Signification : Un CPU élevé peut se manifester par des timeouts de requête parce que le travail est mis en file d’attente en attente d’ordonnancement.
Décision : Si le CPU est saturé, les retries l’empireront. Réduisez la charge, limitez la concurrence et scalez ou optimisez.
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
Signification : Un iowait élevé et un await élevé (48ms) suggèrent que le disque est saturé. Les conteneurs attendant l’I/O peuvent ressembler à des timeouts réseau côté client.
Décision : Si des pics d’await corrèlent avec les timeouts, corrigez l’I/O : déplacez les chemins chauds vers un stockage plus rapide, réduisez les écritures synchrones, ajustez la rotation des logs ou isolez les voisins bruyants.
Task 13: Check container log driver and log pressure
cr0x@server:~$ docker info --format 'LoggingDriver={{.LoggingDriver}}'
LoggingDriver=json-file
Signification : Le driver json-file peut devenir un problème d’I/O si les logs sont volumineux et que la rotation est mal configurée.
Décision : Si le disque est chaud et les logs bruyants, limitez la taille des logs et activez la rotation ; ne maquillez pas le problème en augmentant les timeouts.
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
Signification : exitCode=137 indique un SIGKILL (souvent stop timeout dépassé ou OOM killer). Les flaps de health montrent des seuils de timeout limites.
Décision : Si vous voyez SIGKILL, corrigez l’arrêt. Si vous voyez des flaps de santé, ajustez la durée du healthcheck et enquêtez sur la pression des ressources.
Task 15: Confirm stop behavior (is the app honoring SIGTERM?)
cr0x@server:~$ docker stop -t 10 api-7f3c
api-7f3c
Signification : Ceci demande un arrêt gracieux de 10s. Si cela prend systématiquement plus longtemps ou que le processus est tué, l’application ne vide pas rapidement ses connexions.
Décision : Implémentez la gestion de SIGTERM, cessez d’accepter de nouvelles requêtes, videz les connexions, puis quittez. N’augmentez StopTimeout qu’après avoir justifié ce besoin.
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
Signification : Le pull est rapide pour l’instant. Si les déploiements time-out uniquement en heures de pointe, le registre ou le proxy est en throttling ou votre NAT est stressé.
Décision : Si les pulls sont lents, ajoutez du cache (mirror de registre) ou corrigez l’egress ; ne « retryez » pas indéfiniment pendant les déploiements.
Ajuster les retries correctement : budgets, backoff et jitter
Les retries ne sont pas gratuits. Ils consomment de la capacité, augmentent la latence de queue, et transforment de faibles taux d’erreur en grosses augmentations de trafic.
Pourtant, ce sont aussi d’excellents outils — lorsqu’ils sont bornés et sélectifs.
La mentalité du budget de retry
Commencez par une règle simple : les retries doivent rentrer dans le délai imparti à l’utilisateur. Si une requête a un budget SLO de 2s, vous ne pouvez pas
faire trois tentatives de 2s. Ce n’est pas de la résilience ; c’est se mentir avec des chiffres.
Le « budget » doit prendre en compte :
- Le temps de connexion
- Le temps de traitement serveur
- Le temps de mise en file côté client (pools de threads, exécuteurs async)
- Le délai de backoff entre tentatives
- La variance réseau en pire cas
Quelles erreurs sont retentables ?
Ne retentez que lorsque l’échec est plausiblement transitoire et que l’opération est sûre.
- Bonnes candidates : reset de connexion, 503 temporaire upstream, certains cas de 429 (si vous respectez Retry-After), DNS SERVFAIL (peut-être), GETs idempotents.
- Mauvaises candidates : timeouts avec état serveur inconnu sur opérations non idempotentes (POST ayant pu réussir), 4xx déterministes, échecs d’auth, « payload trop grand ».
- Piègeux : timeouts de lecture — parfois l’upstream est lent, parfois il est mort. Retenter peut doubler la charge sur un service en difficulté.
Backoff et jitter : arrêter les tempêtes de retries synchronisés
Si chaque client retente exactement après 100ms, vous obtenez une foule hurlante : des pics périodiques de trafic qui maintiennent le système dans un état proche de la panne.
Utilisez un backoff exponentiel avec jitter. Oui, le jitter ressemble à de la superstition. Ce n’en est pas ; c’est de la probabilité appliquée.
Une politique par défaut raisonnable pour beaucoup de RPC internes :
- Tentatives max : 2–3 (incluant la première tentative)
- Backoff : exponentiel à partir de 50–100ms
- Jitter : full jitter ou equal jitter
- Timeout par tentative : plus petit que le délai global (par exemple 300ms par tentative dans un budget de 1s)
Ne pas empiler les retries à travers les couches
Si votre application retente, votre sidecar retente, et votre gateway retente, vous avez construit une machine à sous. Elle perd la plupart du temps, mais elle est très confiante.
Choisissez une couche responsable des retries pour un chemin d’appel donné. Faites en sorte que les autres couches observent et appliquent des deadlines, sans amplifier le trafic.
Pourquoi « augmenter simplement le timeout » échoue souvent
Augmenter les timeouts peut aider quand vous avez des opérations rares et bornées qui se termineront si vous attendez un peu plus.
Cela échoue quand :
- Les requêtes sont en file d’attente derrière de la surcharge : des timeouts plus longs ne font que creuser les files.
- Les dépendances sont down : vous occupez inutilement des threads et des sockets plus longtemps.
- Vous masquez un trou noir réseau (MTU, firewall) : attendre ne change pas la physique.
Blague courte #2 : Un timeout est une échéance, pas un choix de style de vie.
Un schéma de réglage concret qui marche
Pour un client HTTP appelant un upstream dans le même cluster/VPC :
- Timeout global de requête : 800ms–2s (selon le SLO)
- Timeout de connexion : 50ms–200ms (échec rapide sur unreachable)
- Timeout par tentative : 300ms–800ms
- Retries : 1 retry pour les requêtes idempotentes (donc 2 tentatives au total)
- Backoff : 50ms → 150ms avec jitter
- Plafond strict sur les requêtes concurrentes en vol (bulkhead)
Le bulkhead n’est pas optionnel. Les retries sans limites de concurrence sont la façon de s’auto-DDOSser son propre upstream.
Démarrage, healthchecks et arrêt : timeouts que vous contrôlez
La plupart des « timeouts de conteneur » qui vous réveillent la nuit sont auto-infligés par une mauvaise configuration du cycle de vie : l’app a besoin de temps pour démarrer,
la plateforme s’attend à de l’instantané, et ensuite tout le monde dispute un graphe.
Démarrage : donnez une période de grâce, pas une laisse plus longue
Si votre service charge des modèles, remplit des caches, exécute des migrations ou attend une dépendance, il va parfois être lent.
Votre travail est de séparer « démarrage » et « unhealthy ».
- Utilisez une période de grâce au démarrage (StartPeriod du healthcheck Docker, ou probes de démarrage de l’orchestrateur).
- Faites des endpoints de santé peu coûteux et conscients des dépendances : « suis-je vivant ? » diffère de « puis-je servir du trafic ? »
- Ne lancez pas de migrations de schéma sur chaque réplique au boot. Ce n’est pas « automatisé » ; c’est une douleur synchronisée.
Healthchecks : petits timeouts, mais pas délirants
Un timeout de healthcheck de 1–2 secondes est correct pour la plupart des endpoints locaux — si le conteneur a du CPU et n’est pas bloqué sur le disque.
Mais si vous le réglez à 200ms parce que « rapide c’est bien », vous n’améliorez pas la fiabilité. Vous augmentez la probabilité de redémarrages sous la variance normale.
Arrêt : traitez-le comme un chemin à part entière
Docker envoie SIGTERM, attend, puis envoie SIGKILL. Si votre app ignore SIGTERM ou bloque en flushant des logs sur un disque lent, elle sera tuée.
Les processus tués abandonnent des requêtes, corrompent l’état et déclenchent des retries côté client — qui ressemblent à des timeouts.
Conseils pratiques :
- Gérez SIGTERM : cessez d’accepter du nouveau travail, drenez, puis quittez.
- Réglez StopTimeout pour couvrir le drain en pire cas, mais gardez-le borné.
- Privilégiez des keepalive et timeouts de requête plus courts afin que le drain se termine vite.
- Rendez la « latence d’arrêt » observable : logguez le début/fin de la terminaison et le nombre de requêtes en vol.
Proxies, load balancers et « chaînes de la mort » multi-timeouts
Une requête utilisateur traverse souvent plusieurs timeouts :
navigateur → CDN → load balancer → ingress proxy → service mesh → app → base de données.
Si ces échéances ne sont pas alignées, la plus courte l’emporte — et ce n’est peut-être pas celle à laquelle vous pensez.
Comment des timeouts mal assortis créent des échecs fantômes
Exemple typique :
- Timeout client : 10s
- Timeout proxy ingress : 5s
- Timeout app vers DB : 8s
À 5 secondes, l’ingress abandonne et ferme la connexion client. L’app continue de travailler jusqu’à 8 secondes, puis annule l’appel DB.
Pendant ce temps, la DB peut encore traiter la requête. Vous venez de transformer une requête lente en travail gaspillé sur trois niveaux.
À quoi ressemble un bon alignement
- Les couches externes ont des timeouts légèrement plus larges que les couches internes, mais pas de façon dramatique.
- Chaque saut applique une échéance et la propage en aval (en-têtes, propagation de contexte).
- Les retries se produisent à une seule couche, avec conscience de l’idempotence et du budget.
Attention aux timeouts d’inactivité
Les timeouts d’inactivité tuent les connexions « silencieuses ». C’est important pour :
- Server-sent events
- WebSockets
- Long-poll
- Grosses uploads où le client fait des pauses
Si vous avez du streaming, réglez explicitement les timeouts d’inactivité et ajoutez des heartbeats applicatifs pour que « inactif » ne soit pas pris pour « mort ».
Stockage et latence I/O : la cause de timeout que personne ne veut
Les ingénieurs aiment déboguer le réseau parce que les outils sont cool et les graphiques sont nets. La latence de stockage est moins glamour : elle apparaît comme « await »
et rend tout le monde triste.
Les conteneurs time-out parce qu’ils attendent l’I/O plus souvent que les équipes ne l’admettent. Coupables fréquents :
- Volume de logs écrivant trop sur des disques lents
- Surcharge de l’overlay filesystem avec beaucoup de petites écritures
- Pannes de stockage réseau (NFS bloqué, congestion iSCSI)
- Bases de données sync-heavy placées sur des nœuds saturés
- Throttling disque au niveau du nœud en environnements virtualisés
Comment la latence de stockage devient « timeout réseau »
Votre handler API écrit une ligne de log, vide un buffer, ou écrit dans un répertoire de cache local.
Cette écriture bloque 200ms–2s parce que le disque est occupé. Votre thread de handler ne peut pas répondre.
Le client voit « awaiting headers » et appelle ça un timeout réseau.
Vous corrigez en :
- Réduisant les écritures synchrones dans les chemins de requête
- Utilisant un logging bufferisé/asynchrone
- Plaçant les workloads stateful sur la bonne classe de stockage
- Séparant les workloads bruyants des workloads sensibles à la latence
Trois mini-histoires du monde de l’entreprise
Mini-histoire 1 : L’incident causé par une mauvaise hypothèse
Une entreprise moyenne exécutait une série de services internes sur des hôtes Docker avec un reverse proxy simple devant. Le proxy avait un timeout upstream de 5 secondes.
Les équipes applicatives supposaient qu’elles disposaient de « 10 secondes » parce que leurs clients HTTP étaient réglés sur 10 secondes. Personne n’avait consigné les réglages du proxy car « c’est juste de l’infrastructure ».
Pendant une maintenance de base de données, la latence des requêtes est passée de <100ms à plusieurs secondes. L’application commençait à répondre vers 6–8 secondes.
Les clients attendaient patiemment. Le proxy non. Il a commencé à couper les connexions à 5 secondes, renvoyant des 504.
Les logs applicatifs étaient trompeurs : les requêtes semblaient « se terminer », mais le client avait déjà abandonné. Certains clients ont retry automatiquement. Désormais la même requête coûteuse
s’exécutait deux fois, parfois trois. La charge sur la base a augmenté, la latence a augmenté, et davantage de requêtes franchissaient la barrière des 5 secondes du proxy. Un simple incident de maintenance est devenu un vrai incident.
La correction a été embarrassante de simplicité : aligner les deadlines. Le timeout du proxy est devenu légèrement supérieur au timeout DB de l’app, et le timeout client de l’app est devenu légèrement supérieur à celui du proxy.
Ils ont aussi désactivé les retries pour les endpoints non idempotents et ajouté des clés d’idempotence quand nécessaire. Le mieux : la prochaine maintenance fut ennuyeuse, ce qui est l’issue correcte.
Mini-histoire 2 : L’« optimisation » qui s’est retournée contre eux
Une autre organisation souhaitait un failover plus rapide. Quelqu’un a réduit les timeouts d’appel de service de 2 secondes à 200ms et augmenté les retries de 1 à 5 « pour compenser ».
Ça semblait astucieux : détection rapide, multiples tentatives, moins d’erreurs visibles par l’utilisateur. En staging, cela paraissait fonctionner — staging manque rarement de mise en file ou de congestion réelle.
En production, un service aval avait des pics de latence occasionnels de 300–600ms lors de GC et de rafraîchissements de cache périodiques. Avec les nouveaux réglages, la variance normale devenait une défaillance.
Les clients atteignaient 200ms, time-outaient, retentaient, time-outaient encore, et ainsi de suite. Ils échouaient non pas plus vite mais plus bruyamment.
Le service aval n’a pas seulement vu plus de trafic ; il a vu du trafic synchronisé en rafales. Cinq retries sans jitter signifiaient des vagues de charge toutes les quelques centaines de millisecondes.
Le CPU a monté. La latence tail s’est dégradée. L’aval a pris du retard et a commencé à timeouter pour de vrai. Maintenant les upstreams brûlaient aussi du CPU sur les retries et saturaient les pools de connexions.
Le rollback de l’« optimisation » a stabilisé le système en minutes. La solution durable fut : un seul retry avec backoff exponentiel et jitter,
un timeout par tentative calé sur le p95 observé, et un plafond global de concurrence. Ils ont aussi ajouté des dashboards montrant le taux de retries et l’amplification QPS effective.
La leçon cachée : du travail de résilience qui ignore les queues de distribution n’est que de l’optimisme en YAML.
Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Une équipe de services financiers exécutait des workers batch Docker qui appelaient une API externe. L’API renvoyait parfois des 429 en cas de throttling.
L’équipe avait une politique terne : respecter Retry-After, plafonner les retries à 2, et appliquer une échéance dure par job. Pas d’héroïsme, pas de boucles infinies.
Un après-midi, le fournisseur externe a eu une panne partielle. Beaucoup de clients ont subi des échecs en cascade parce que leurs clients retentaient agressivement et pilonnaient l’API déjà en difficulté.
Les workers de cette équipe ont ralenti au lieu d’accélérer. Les jobs ont pris plus de temps, mais le système est resté stable.
Leurs dashboards montraient des 429 élevés et des durées de job augmentées, mais la file n’a pas explosé. Pourquoi ? Budget de retry plus backpressure.
Ils avaient aussi un coupe-circuit : après un seuil d’échecs, les workers arrêtaient d’appeler l’API pendant une courte fenêtre de refroidissement.
Quand le fournisseur a récupéré, la backlog de l’équipe s’est drainée de façon prévisible. Pas d’auto-scaling d’urgence, pas de timeouts mystères, pas de débats « on devrait ajouter plus de retries ».
Les pratiques ennuyeuses ne font pas de conférences. Elles vous évitent les ponts d’incident, ce qui est mieux.
Erreurs courantes : symptôme → cause racine → correctif
1) Symbole : les timeouts augmentent exactement quand un upstream ralentit
Cause racine : les retries amplifient la charge ; plusieurs couches retentent ; pas de backoff/jitter.
Correctif : réduire les tentatives (souvent à 2 au total), ajouter backoff exponentiel avec jitter, imposer des limites de concurrence, et s’assurer qu’une seule couche retente.
2) Symbole : « dial tcp … i/o timeout » sur de nombreux services
Cause racine : table conntrack pleine, perte de paquets, mismatch MTU ou firewall qui droppe des SYN/ACK.
Correctif : vérifier l’utilisation du conntrack, réduire le churn de connexions via keepalive/pooling, augmenter le max conntrack prudemment, et valider le MTU de bout en bout.
3) Symbole : les conteneurs redémarrent et les logs montrent qu’ils allaient « bien »
Cause racine : timeout de healthcheck trop agressif ; absence de grâce au démarrage ; vérifications de dépendance dans le liveness.
Correctif : séparer logique liveness vs readiness, ajouter StartPeriod (ou probe de démarrage), régler le timeout pour correspondre au p95 réaliste de l’endpoint de santé sous charge.
4) Symbole : les requêtes time-outent pendant les déploiements, pas en steady state
Cause racine : arrêt non gracieux ; StopTimeout trop court ; drain de connexion non implémenté ; LB continue d’envoyer au tâches en terminaison.
Correctif : implémenter le drain SIGTERM, augmenter StopTimeout de façon appropriée, configurer la désinscription/délai de drain du LB, et réduire les keepalive/timeouts pour sortir plus vite.
5) Symbole : les timeouts « awaiting headers » empirent quand le logging est verbeux
Cause racine : saturation I/O disque due au volume de logs ou au driver json-file ; overhead de l’overlay filesystem.
Correctif : plafonner/rotater les logs, expédier les logs de façon asynchrone, déplacer les disques chauds vers SSD/NVMe, isoler les workloads stateful, et mesurer l’await iostat pendant les incidents.
6) Symbole : les résolutions DNS prennent parfois des secondes
Cause racine : retries du résolveur, DNS surchargé, mauvaise configuration des domaines de recherche, DNS embarqué goulot d’étranglement, ou perte de paquets intermittente.
Correctif : mesurer la latence des lookups dans les conteneurs, simplifier domaines de recherche/options, ajouter du caching, et s’assurer que les serveurs DNS ont la capacité et peu de perte.
7) Symbole : connexions longue durée drops autour du même intervalle
Cause racine : timeouts d’inactivité sur LB/proxy ; absence de keepalives ou heartbeats.
Correctif : aligner les timeouts d’inactivité entre couches et ajouter des heartbeats applicatifs pour le streaming/WebSockets.
8) Symbole : augmenter les timeouts rend tout « pire mais plus lent »
Cause racine : vous êtes surchargé ; des timeouts plus longs creusent les files et augmentent la durée de rétention des ressources.
Correctif : shed load, réduire la concurrence, scaler la capacité, et raccourcir les timeouts internes pour que les échecs libèrent vite les ressources.
Listes de contrôle / plan étape par étape
Étape par étape : corriger les timeouts de conteneur sans retries infinis
- Classifiez le timeout : connexion vs lecture vs inactivité vs arrêt. Utilisez logs et métriques proxy pour identifier où il se déclenche.
- Trouvez la plus courte échéance dans la chaîne : CDN/LB/ingress/mesh/app/db. Le plus petit timeout gouverne l’expérience utilisateur.
- Mesurez p50/p95/p99 pour le chemin d’appel. Réglez pour les queues, pas pour la médiane.
- Fixez une échéance globale par requête basée sur le SLO et l’UX (ce que les utilisateurs tolèrent).
- Scindez les timeouts : short connect timeout, bounded read timeout, et une deadline globale.
- Choisissez un seul propriétaire de retry (lib client ou mesh, pas les deux). Désactivez les retries ailleurs.
- Limitez les retries : typiquement 1 retry pour les appels idempotents. Plus d’essais nécessitent des preuves et un budget plus large.
- Ajoutez un backoff exponentiel avec jitter, toujours. Aucune exception pour le trafic « interne ».
- Ajoutez des bulkheads : plafonnez la concurrence et la longueur des files ; préférez échouer vite plutôt que laisser les files croître.
- Rendez les opérations non sûres sûres : clés d’idempotence pour POST/PUT quand la logique métier le permet.
- Corrigez les timeouts de cycle de vie : StartPeriod du healthcheck, timeout de santé réaliste, et StopTimeout aligné au drain.
- Validez les contraintes hôte : marge conntrack, perte de paquets, throttling CPU, await disque.
- Prouvez que le changement a fonctionné : observez le taux de retries, l’amplification QPS upstream, la latence p99 et la consommation du budget d’erreurs.
Checklist rapide : quoi changer d’abord (ROI le plus élevé)
- Supprimez les retries infinis. Remplacez par un nombre maximal d’essais et une deadline.
- Ajoutez du jitter à toute boucle de retry/backoff.
- Assurez-vous qu’une seule couche retente.
- Corrigez StartPeriod des healthchecks et les timeouts trop courts.
- Vérifiez l’utilisation du conntrack et l’await disque pendant les incidents.
FAQ
1) Dois-je jamais utiliser des retries infinis ?
Presque jamais. Les retries infinis ne conviennent qu’à des systèmes d’arrière-plan strictement contrôlés avec backpressure explicite, files durables et gestion opérateur visible des dead-letters.
Pour les requêtes orientées utilisateur, les retries infinis transforment les pannes en désastres au ralenti.
2) Combien de retries est « sûr » pour des appels internes ?
Communément : un retry pour les opérations idempotentes, avec backoff et jitter, et seulement si vous avez de la capacité disponible. Si l’upstream est surchargé, les retries ne sont pas « sûrs », ce sont de l’essence.
3) Quelle est la différence entre connect timeout et read timeout ?
Le connect timeout couvre l’établissement de la connexion (routage, SYN/ACK, handshake TLS). Le read timeout couvre l’attente de la réponse une fois la connexion établie.
Les connect timeouts pointent vers le réseau/conntrack/firewall. Les read timeouts pointent vers la latence upstream, la mise en file ou les blocages I/O.
4) Pourquoi les timeouts augmentent-ils pendant les déploiements ?
Parce que l’arrêt et la readiness sont souvent mal gérés. Les conteneurs en terminaison reçoivent encore du trafic, ou acceptent des requêtes alors qu’ils ne sont pas ready, ou sont SIGKILLés en plein traitement.
Corrigez le drain, les StopTimeout et le timing de désinscription du load balancer.
5) Comment savoir si les retries provoquent une tempête de retries ?
Cherchez une montée du QPS upstream qui ne correspond pas au trafic utilisateur, plus une augmentation du taux d’erreur et de la latence p99. Vérifiez aussi si les échecs forment des vagues périodiques (manque de jitter).
Suivez « tentatives par requête » si possible.
6) Les timeouts de healthcheck sont-ils la même chose que les timeouts de requête ?
Non. Les healthchecks sont la décision de la plateforme de tuer ou de router vers un conteneur. Les timeouts de requête sont la décision des clients d’attendre ou non.
Un mauvais healthcheck peut ressembler à des « timeouts aléatoires » car il supprime de la capacité ou redémarre le service en cours de charge.
7) Pourquoi le DNS compte-t-il autant dans les conteneurs ?
Parce que chaque appel de service commence par une résolution de nom à moins que vous ne pinnez des IPs (ne le faites pas). Si le DNS est lent, chaque requête paie cet impôt, et les retries le multiplient.
Le DNS en conteneur ajoute une couche supplémentaire (DNS embarqué ou cache node-local) qui peut devenir un goulot.
8) Quand augmenter un timeout est-il la bonne solution ?
Quand vous avez prouvé que le travail se termine avec un peu plus de temps et que vous avez la capacité pour garder les ressources occupées plus longtemps.
Exemple : un endpoint de génération de rapport lent mais borné, avec faible concurrence et file d’attente appropriée.
9) Quelle est la meilleure façon d’éviter les doublons lors du retry de POST ?
Utilisez des clés d’idempotence (IDs de requête générés côté client stockés côté serveur), ou concevez l’opération pour qu’elle soit idempotente.
Si vous ne pouvez pas, ne retentez pas automatiquement — exposez l’échec et laissez un humain ou un système de jobs réconcilier.
10) Comment empêcher les timeouts de se propager à travers les services ?
Utilisez des deadlines propagées de bout en bout, des bulkheads (limites de concurrence), des circuit breakers, et des retries bornés avec jitter.
Gardez aussi les timeouts internes plus courts que les externes afin que l’échec s’arrête plus vite en aval.
Conclusion : prochaines étapes pratiques
Les timeouts ne sont pas l’ennemi. L’attente non bornée l’est. Si vous voulez moins d’incidents, cessez de traiter les retries comme de la magie et commencez à les considérer comme une dépense contrôlée.
- Choisissez une échéance de requête qui correspond à la réalité (SLO et tolérance utilisateur).
- Scindez connect/read timeouts et logguez-les distinctement.
- Limitez les retries (généralement un) et ajoutez un backoff exponentiel avec jitter.
- Assurez-vous qu’une seule couche retente ; partout ailleurs doit appliquer des deadlines.
- Corrigez le tuning du cycle de vie : StartPeriod du healthcheck, timeout de santé réaliste, arrêt gracieux avec StopTimeout suffisant.
- Pendant le prochain incident de timeout, lancez le playbook de diagnostic rapide et les tâches ci-dessus — en particulier conntrack, latence DNS et await disque.
Si vous ne faites que deux choses cette semaine : supprimez les retries infinis et alignez les timeouts entre proxies et services. Vous sentirez la différence la prochaine fois que la latence fera des siennes.