← Tous les cours / Timeouts — du handshake au circuit breaker Cours
00 — VUE D'ENSEMBLE

Un timeout n'annule rien

Un timeout, c'est décider d'arrêter d'attendre. Subtilité capitale, et source de la moitié des bugs de production : arrêter d'attendre ne veut pas dire arrêter le travail à l'autre bout.

Un timeout, dans son principe, est d'une simplicité trompeuse : une partie attend une ressource ou la fin d'une action, et fixe une durée au-delà de laquelle elle cesse d'attendre. C'est tout. Mais cette définition cache le piège le plus coûteux du domaine.

Un timeout n'annule pas l'action
Cesser d'attendre et annuler le travail sont deux choses orthogonales. Tu peux abandonner l'attente alors que le traitement continue tranquillement à l'autre bout. Pour réellement l'arrêter, il faut un mécanisme séparé. Confondre les deux, c'est gaspiller des ressources sur du travail dont plus personne n'attend le résultat.

Pourquoi met-on des timeouts ? Pour trois raisons qui reviennent sans cesse : libérer des ressources (mémoire, CPU, workers) au lieu de les laisser bloquées ; résister aux attaques par épuisement (quelqu'un qui traîne exprès pour saturer tes workers) ; et détecter les opérations lentes qui dépassent tes SLA pour les diagnostiquer.

Au fil d'une requête, on rencontre cinq timeouts distincts, à des étages différents — et il faut comprendre que chacun est à la fois client et serveur : ton backend appelle d'autres backends, un CDN agit en client face à ton origine. Tout le monde attend quelqu'un.

connexion
lecture
attente
traitement
réponse
💡 Le fil du cours : on suit une requête de la poignée de main jusqu'à la réponse, en posant un timeout à chaque étage. Puis on va plus loin que la simple énumération : annuler pour de vrai, les timeouts qui se cumulent derrière un proxy, et surtout comment réessayer sans déclencher une réaction en chaîne.
01 — CONNEXION

Le timeout de connexion

Avant le moindre octet de requête, il faut établir la connexion : poignée de main TCP, puis session TLS. Si ça traîne, le client doit pouvoir renoncer — c'est le premier timeout.

Avant d'échanger quoi que ce soit, le client doit établir une connexion avec le serveur. Quand on dit « connexion », on parle en réalité de deux choses empilées : la connexion TCP (la poignée de main en trois temps — SYN, SYN-ACK, ACK) et, par-dessus, la session TLS qui chiffre le canal.

Timeout de connexion (côté client)
Si la poignée de main TCP traîne (le serveur tarde à répondre au SYN) ou si la négociation TLS s'éternise, le client renonce après un délai. C'est un timeout côté client : « j'ai essayé de me connecter, je n'y suis pas arrivé à temps, j'arrête — et je réessaierai peut-être plus tard ».

Ce timeout est court par nature : établir une connexion sur un réseau sain prend des millisecondes. Le laisser trop long, c'est garder des tentatives de connexion bloquées pour rien — exactement ce qu'un attaquant cherche à provoquer.

Son cousin discret : le timeout d'inactivité (keep-alive). Une fois la connexion établie, on la garde souvent ouverte pour enchaîner plusieurs requêtes (HTTP keep-alive). Mais une connexion ouverte qui ne sert plus consomme une ressource. Le timeout d'inactivité la ferme après un silence trop long. C'est lui, par exemple, qui coupe une session WebSocket inactive (10 minutes côté API Gateway — voir le cours Chat temps réel serverless).

La mécanique de TCP (handshake, SYN, fermeture) est détaillée dans TCP & Sockets, et la couche TLS dans TLS/HTTPS.

02 — LECTURE

Le timeout de lecture (et Slowloris)

Une fois connecté, le client écrit sa requête — et le serveur la lit, octet par octet. Un client malveillant peut l'envoyer au compte-gouttes pour bloquer un worker. D'où un timeout de lecture.

La connexion est établie ; le client peut maintenant écrire sa requête. Côté serveur, recevoir cette requête n'est pas instantané : il lit un flux d'octets (chiffrés, qu'il déchiffre au passage) et cherche le début, puis la fin, d'une requête bien formée. Tant qu'il n'a pas tout lu, il ne peut pas traiter.

Timeout de lecture (côté serveur)
Combien de temps le serveur accepte-t-il de passer à lire une requête ? Si le client est lent à transmettre (gros upload, ou pire, malveillance), le serveur ne va pas attendre indéfiniment.

Ce timeout n'est pas un détail de performance : c'est une défense. Il existe une attaque conçue exactement pour l'exploiter.

⚠️ Slowloris (slow-roll). L'attaquant envoie sa requête HTTP un octet à la fois, très lentement, sans jamais la terminer. Le serveur garde un worker mobilisé à lire, encore et encore. Multiplie par des milliers de connexions, et tous les workers sont occupés à attendre des requêtes qui n'arrivent jamais — le serveur est à genoux sans le moindre trafic réel. Un timeout de lecture serré tue l'attaque.

C'est à toi, qui connais ton système, de calibrer : la plupart des requêtes sont minuscules (un GET avec ses en-têtes, cookies et jeton tient en 1 à 2 Ko). Lire ça ne doit pas prendre plus d'une ou deux secondes. Inutile d'être généreux.

Le bon code : 408, pas 403
Quand le serveur abandonne la lecture d'une requête incomplète, le code HTTP correct est 408 Request Timeout — « je n'ai pas reçu ta requête complète à temps ». (On confond parfois avec d'autres 4xx ; c'est bien 408, et c'est un 4xx car la faute est côté client qui n'a pas su envoyer à temps.)

Le simulateur ci-dessous oppose un client normal à un client Slowloris. Lance les deux et regarde le timeout de lecture trancher.

Compare un client normal et un client lent :

03 — ATTENTE EN FILE

Le timeout d'attente

Une requête lue n'est pas forcément traitée tout de suite : souvent elle attend dans une file qu'un worker se libère. Combien de temps tolérer cette attente avant d'abandonner ?

La requête est lue. Va-t-elle être traitée tout de suite ? Pas forcément. Il existe plusieurs façons d'exécuter une requête, et toutes n'exécutent pas immédiatement.

ModèlePrincipePour
SynchroneLe thread qui lit la requête l'exécute aussitôtRequêtes courtes
Via une fileLa requête est mise en file ; un pool de workers la dépile plus tardRequêtes longues, charge à lisser

Le modèle en file coûte un peu d'overhead, mais il rend l'exécution mesurable et contrôlable : on voit combien de requêtes attendent, depuis combien de temps, et on peut même les prioriser. En échange, une requête peut patienter avant qu'un worker se libère — d'où un nouveau timeout.

Timeout d'attente (côté serveur)
Combien de temps une requête peut-elle rester dans la file avant qu'un worker la prenne ? Au-delà, on l'abandonne sans même l'avoir traitée — et on prévient le client que sa requête a expiré en attente. C'est un signal de saturation : la file grossit plus vite qu'elle ne se vide.
💡 Le lien avec le rate limiting. Abandonner une requête qui attend trop, c'est une forme de load shedding : on sacrifie quelques requêtes pour protéger le système. C'est cousin du Rate Limiting, et toute la mécanique des files (workers, priorités) est détaillée dans Files de messages.

Le simulateur ci-dessous a deux workers et une file. Empile des requêtes : celles qui patientent au-delà du seuil expirent avant d'être traitées.

Empile des requêtes plus vite que les workers ne les traitent :

04 — TRAITEMENT

Le timeout de traitement

C'est le timeout auquel tout le monde pense : combien de temps une requête peut-elle accaparer le serveur avant qu'on la coupe ? Et, surprise, HTTP n'a pas de code standard pour ça.

Un worker a pris la requête : place au traitement. C'est le timeout auquel tout le monde pense en entendant le mot : combien de temps une requête peut-elle accaparer le serveur avant qu'on la coupe net ?

Timeout de traitement (côté serveur)
La durée maximale d'exécution d'une requête. Au-delà, on la termine de force pour récupérer le worker. Imagine une requête qui calcule tous les nombres premiers jusqu'au milliard, ou une lecture massive en base suivie d'un tri coûteux : sans garde-fou, elle monopolise un worker indéfiniment.

La bonne valeur dépend entièrement du type de requête. Un endpoint de lecture rapide ne devrait jamais dépasser une poignée de secondes ; un export ou un calcul lourd a légitimement besoin de plus. Les backends soignés appliquent donc des timeouts différenciés par route, plutôt qu'un seul plafond global.

⚠️ Pas de code HTTP standard pour ça. Contrairement à la lecture (qui a son 408), il n'existe pas de code HTTP officiel pour « le traitement a dépassé le temps imparti ». Selon les cas, on renvoie un 503, un 500, ou — quand un proxy est dans la boucle — un 504 (voir la section Proxies). C'est une décision applicative, pas une norme.

Et comme toujours : couper le traitement après coup ne récupère pas le temps déjà brûlé. Mieux vaut détecter tôt les requêtes qui partent en vrille — ce qui rejoint la section suivante, sur ce qui se passe quand le client, lui, a déjà renoncé.

05 — RÉPONSE

Le timeout de réponse

Côté client : j'ai envoyé ma requête, combien de temps j'attends la réponse avant de renoncer ? Et renoncer côté client ne dit rien au serveur — il continue, tranquillement.

Changeons de point de vue : on est maintenant le client. J'ai envoyé ma requête, et j'attends la réponse. Combien de temps avant de renoncer ?

Timeout de réponse (côté client)
La durée pendant laquelle le client attend une réponse avant d'abandonner. Il s'applique surtout aux échanges synchrones (« j'envoie, j'attends »). À noter : certains flux n'en veulent pas — un flux d'événements (Server-Sent Events) est par nature sans fin, donc le navigateur n'impose pas de timeout de réponse dessus.

Côté navigateur, on déclenche soi-même cet abandon avec un AbortController et son AbortSignal, passé à fetch :

const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 5000);   // renonce après 5 s

await fetch('/api/rapport', { signal: ctrl.signal });
⚠️ Abandonner n'arrête pas le serveur. Quand tu abort(), tu cesses simplement d'attendre. Le serveur, lui, n'en sait rien : il continue de traiter ta requête jusqu'au bout, et de consommer des ressources pour une réponse que plus personne ne lira. C'est le piège central des timeouts — on l'attaque de front à la section suivante.
06 — ANNULER VRAIMENT

Abandonner n'est pas annuler

Tu fermes l'onglet, tu coupes la connexion — et le serveur continue de calculer dans le vide. Annuler pour de vrai demande une mécanique explicite, et un peu de comptabilité.

On y revient, parce que c'est le cœur du sujet : un timeout n'annule rien. Quand un client renonce, le serveur poursuit son travail. Pour l'arrêter pour de vrai, il faut une mécanique explicite — et un peu de comptabilité.

Fermer la connexion ne suffit pas (tout seul)

« Je ferme la connexion, donc tout s'arrête », entend-on souvent. Pas si vite. Fermer une connexion envoie bien un FIN (ou un RST) en TCP, et le noyau du serveur le constate. Mais le noyau doit ensuite notifier l'application, et c'est à l'application d'avoir une logique qui dit : « la connexion sur laquelle je traitais cette requête vient de tomber → j'annule le travail associé ».

La comptabilité connexion → requêtes
Rien n'est magique : pour annuler, le serveur doit savoir quelle requête appartient à quelle connexion. Il faut maintenir cette association, et réagir à un événement « connexion fermée » en allant interrompre le traitement correspondant — une boucle qui vérifie un drapeau d'annulation, une requête SQL qu'on interrompt, etc.

Une approche plus brutale : si ton pool dédie un thread par requête, tu peux tuer le thread du client parti. Ça marche, mais c'est lourd — il faut ensuite recréer un thread (ou pire, un process), ce qui prend du temps et peut déclencher… le timeout d'attente pour les requêtes suivantes. Tout est lié.

Les outils du protocole

Au-dessus de TCP, les protocoles modernes offrent l'annulation proprement : HTTP/2 a la trame RST_STREAM pour annuler un seul stream sans toucher à la connexion, et gRPC propage des annulations de bout en bout. Mais l'outil ne fait pas la logique : c'est toujours à ton application de réagir au signal. (Les streams HTTP/2 et leur cycle de vie : HTTP · De 1.1 à 3.)

Le simulateur ci-dessous rend l'orthogonalité tangible. Lance un traitement long, puis « abandonne » — et regarde le serveur continuer. Puis essaie une vraie annulation.

Lance, puis abandonne (le serveur continue) — ou annule vraiment :

07 — PROXIES & CASCADE

Quand les timeouts se cumulent

Tout le monde est à la fois client et serveur. Glisse un proxy ou un CDN au milieu, et chaque timeout se rejoue à chaque maillon — avec, en prime, le fameux 504.

Souviens-toi du principe de l'intro : tout le monde est à la fois client et serveur. Glisse un proxy, un reverse proxy ou un CDN au milieu, et la vérité éclate : chaque timeout se rejoue à chaque maillon.

Tu parles au CDN. De son point de vue, c'est lui le serveur : il a un timeout de connexion avec toi, un timeout de lecture de ta requête. Puis il se retourne vers l'origine et devient à son tour un client : nouveau timeout de connexion, nouveau timeout de réponse, cette fois envers ton backend.

Client
CDN / proxy
Origine
502 vs 504
Quand le proxy n'obtient pas satisfaction de l'origine, il le signale au client :
504 Gateway Timeout — l'origine n'a pas répondu à temps (ou la connexion vers elle a échoué).
502 Bad Gateway — l'origine a répondu, mais avec quelque chose d'invalide.
Le 504, c'est littéralement un timeout de réponse vu par le proxy.

Ces timeouts s'empilent, et c'est un piège : si chaque maillon attend généreusement, le client final attend la somme. D'où une discipline essentielle, que la vidéo source n'aborde pas mais qui change tout en production.

Propagation de deadline (timeout budget)
Plutôt que chaque service ait son propre timeout dans son coin, on propage une échéance à travers la chaîne. Le premier maillon fixe : « cette requête doit aboutir en 3 s ». Il reste 3 s. Le maillon suivant en a déjà consommé 1, il transmet « il reste 2 s » au suivant, et ainsi de suite. Si le budget est épuisé, on arrête immédiatement — inutile de lancer un travail déjà condamné. gRPC en fait un mécanisme natif (les deadlines).
💡 Règle d'or des timeouts empilés : le timeout d'un appelant doit être plus court que celui de l'appelé, jamais l'inverse — sinon l'appelant renonce alors que l'appelé travaille encore, et tu cumules le pire des deux mondes. Les CDN et reverse proxies, et leurs timeouts, sont l'objet de Reverse Proxy, CDN & Cache.

Le simulateur ci-dessous est une chaîne client → proxy → origine. Rends l'origine lente et observe d'où vient le 504.

Règle la vitesse de l'origine et envoie une requête à travers le proxy :

08 — RETRIES & RÉSILIENCE

Réessayer sans tout faire tomber

Un timeout déclenche souvent un retry. Mal fait, c'est l'étincelle qui transforme un service lent en panne totale. Backoff, jitter, idempotence et circuit breaker sont les garde-fous.

Un timeout débouche presque toujours sur une question : est-ce que je réessaie ? C'est tentant — et c'est l'une des décisions les plus dangereuses de l'ingénierie distribuée. Mal faite, elle transforme un service simplement lent en panne totale.

🌩️ La tempête de retry (thundering herd). Un service ralentit ; ses clients atteignent leur timeout de réponse et réessaient. Mais — on l'a vu — le retry n'annule pas la première requête, toujours en cours. Le service reçoit donc une deuxième requête, puis une troisième… La charge explose au moment précis où le service est déjà à la peine. Et quand il tente de se relever, la horde de retries le fait retomber aussitôt. De grandes pannes ont été amplifiées exactement par ce mécanisme.

La parade n'est pas « ne jamais réessayer », mais réessayer intelligemment. Quatre garde-fous.

1. Backoff exponentiel + jitter
Au lieu de réessayer aussitôt, on attend de plus en plus longtemps entre deux tentatives (1 s, 2 s, 4 s, 8 s…) : c'est le backoff exponentiel. Mais si tous les clients suivent la même courbe, ils réessaient tous en même temps — la horde frappe en rafales synchronisées. On ajoute donc du jitter : un délai aléatoire qui désynchronise les clients et étale la charge.
2. Idempotence
Ne réessaie que ce qui est sûr à rejouer. Une lecture, oui. Un « débite 100 € », non — sauf si l'opération est idempotente (une clé d'idempotence garantit qu'un même ordre exécuté deux fois ne compte qu'une fois). C'est le même réflexe que pour les files de messages (livraison « au moins une fois »).
3. Circuit breaker (disjoncteur)
Comme un disjoncteur électrique. Après un certain nombre d'échecs, le circuit s'ouvre : les appels suivants échouent immédiatement, sans même solliciter le service en difficulté — ce qui lui laisse le temps de respirer. Après un délai, le circuit passe en demi-ouvert : il laisse passer un appel test. S'il réussit, le circuit se referme ; sinon, il se rouvre.
4. Retry budget & ne réessaie que le transitoire
On plafonne la part de retries (par ex. « au plus 10 % de requêtes en plus »), et on ne réessaie que les erreurs transitoires (timeout, 503) — jamais un 400 ou un 404, qui échoueront pareil.

Le simulateur ci-dessous met un backend fragile face à trois stratégies. Compare ce qu'il advient de sa santé.

Un backend en difficulté reçoit une rafale. Choisis la stratégie :

09 — SYNTHÈSE

Mettre des timeouts partout

Cinq timeouts, plus l'inactivité, plus la cascade des proxies. Récapitulons la carte — et le principe : pas de mise en attente sans limite, nulle part.

Une requête traverse plusieurs attentes, et chacune mérite sa limite. Voici la carte complète.

TimeoutCôtéQuestionSignal typique
ConnexionClientLa connexion (TCP+TLS) s'établit-elle à temps ?échec de connexion
InactivitéLes deuxLa connexion ouverte sert-elle encore ?fermeture
LectureServeurLa requête arrive-t-elle entièrement à temps ?408
AttenteServeurCombien patienter en file avant un worker ?503
TraitementServeurCombien de temps exécuter avant de couper ?pas de code standard
RéponseClientCombien attendre la réponse ?abandon (abort)
GatewayProxyL'origine répond-elle au proxy à temps ?504
🚫 Le réflexe à graver : aucune attente sans limite, nulle part. Une attente non bornée est une ressource qu'un incident — ou un attaquant — finira par épuiser. Et un timeout sans stratégie d'annulation ni de retry maîtrisé déplace simplement le problème.
Ce que tu emportes
Un timeout décide d'arrêter d'attendre — pas d'arrêter le travail : les deux demandent des mécaniques distinctes. Les timeouts se cumulent le long d'une chaîne, donc on propage une échéance et on garde l'appelant plus impatient que l'appelé. Et réessayer sans backoff, jitter, idempotence ni circuit breaker, c'est armer une tempête de retry.

Pour les territoires voisins : TCP & Sockets et TLS/HTTPS (la connexion), Reverse Proxy, CDN & Cache (la cascade), Files de messages (l'attente et l'idempotence), Rate Limiting (le load shedding) et Fondamentaux de l'architecture backend (la vue d'ensemble).