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.
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.
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.
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.
La mécanique de TCP (handshake, SYN, fermeture) est détaillée dans TCP & Sockets, et la couche TLS dans TLS/HTTPS.
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.
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.
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.
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 :
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èle | Principe | Pour |
|---|---|---|
| Synchrone | Le thread qui lit la requête l'exécute aussitôt | Requêtes courtes |
| Via une file | La requête est mise en file ; un pool de workers la dépile plus tard | Requê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.
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 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 :
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 ?
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.
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é.
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 ?
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 });
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.
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é ».
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 :
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.
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.
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).
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 :
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 parade n'est pas « ne jamais réessayer », mais réessayer intelligemment. Quatre garde-fous.
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.
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 »).
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.
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 :
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.
| Timeout | Côté | Question | Signal typique |
|---|---|---|---|
| Connexion | Client | La connexion (TCP+TLS) s'établit-elle à temps ? | échec de connexion |
| Inactivité | Les deux | La connexion ouverte sert-elle encore ? | fermeture |
| Lecture | Serveur | La requête arrive-t-elle entièrement à temps ? | 408 |
| Attente | Serveur | Combien patienter en file avant un worker ? | 503 |
| Traitement | Serveur | Combien de temps exécuter avant de couper ? | pas de code standard |
| Réponse | Client | Combien attendre la réponse ? | abandon (abort) |
| Gateway | Proxy | L'origine répond-elle au proxy à temps ? | 504 |
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).