Vol. 7 · № 01 · Un atelier d'architectureAn architecture workshop

Retry, Timeout & Backoff, ou l'échec qu'on prévoit au lieu de le subirRetry, Timeout & Backoff, or the failure you plan for instead of enduring

Le Vol 6 a tracé la frontière réseau ; ce numéro affronte ce qu'elle rend inévitable. Un appel entre services peut être lent, expirer, ou échouer alors que le service va très bien — le renversement qui fonde le volume : l'échec transitoire n'est pas une exception rare, c'est un état normal du fil. Trois outils le domptent : un timeout qui borne l'attente, un retry qui absorbe le transitoire, un backoff avec jitter qui espace les tentatives. Mais retenter un appel déjà abouti double l'effet — d'où l'idempotence. La règle : ne retente que ce qui est transitoire et idempotent, borné, à un seul étage — sinon échoue vite.Vol 6 drew the network boundary; this issue faces what it makes inevitable. A call between services can be slow, time out, or fail while the service is perfectly fine — the reversal that founds the volume: transient failure isn't a rare exception, it's a normal state of the wire. Three tools tame it: a timeout that bounds the wait, a retry that absorbs the transient, a backoff with jitter that spaces attempts. But retrying an already-landed call doubles the effect — hence idempotency. The rule: retry only what is transient and idempotent, bounded, at a single tier — otherwise fail fast.

AudienceAudience
Dev qui découvre que sur le réseau, l'échec n'est pas l'exceptionDev discovering that on the network, failure isn't the exception
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026June 2026
≈ 18 min●●○○Résilience distribuéeRetryTradeoff
SommaireContents ·
№ 01CadrageFraming3 min

L'autre côté ne répond pas. Est-ce un bug, ou l'état normal du réseau ?The other side doesn't answer. Is it a bug, or the network's normal state?

Le Vol 6 a tracé la frontière réseau ; ce numéro affronte ce qu'elle rend inévitable. Un appel entre services peut être lent, expirer, ou échouer alors que le service va parfaitement bien — un paquet perdu, un redéploiement, une microseconde de surcharge. Écrit naïvement, l'appel attend l'infini, puis ou bien abandonne au premier hoquet, ou bien réessaie en boucle et aggrave la panne. Ce qui varie d'un appel à l'autre, ce n'est pas s'il peut échouer : c'est comment on traite cet échec. Le réflexe : considérer l'échec transitoire comme un état normal du réseau, et le gérer avec trois outils — un timeout pour ne pas attendre l'infini, un retry pour absorber le transitoire, un backoff pour ne pas réessayer en rafale.Vol 6 drew the network boundary; this issue faces what it makes inevitable. A call between services can be slow, time out, or fail while the service is perfectly fine — a dropped packet, a redeploy, a microsecond of overload. Written naively, the call waits forever, then either gives up at the first hiccup, or retries in a loop and worsens the outage. What varies between calls isn't whether they can fail: it's how you treat that failure. The reflex: treat transient failure as a normal state of the network, and handle it with three tools — a timeout so you don't wait forever, a retry to absorb the transient, a backoff so you don't retry in bursts.

Le code naïf — un appel qui attend l'infini, puis paniqueThe naive code — a call that waits forever, then panics
# un appel inter-services écrit comme s'il répondait toujours, vite, et une seule fois
def charge(order_id, amount):
    resp = http.post("http://billing/charge", json={...})   # 💥 aucun timeout : attend l'infini
    return resp.json()["status"]                            #     si billing rame, on rame avec lui

# et le jour où ça échoue, deux réflexes naïfs, tous deux faux :
#   - on n'essaie pas → un hoquet réseau d'1s fait échouer une commande qui aurait abouti
#   - on réessaie en boucle, tout de suite → on tape un service déjà au sol, et on
#     re-poste sans clé : si le 1er appel avait débité avant le timeout, on débite DEUX fois.
# l'échec réseau n'est pas une exception rare : c'est un état NORMAL du fil.
L'échec-exception vs l'échec-état normalFailure-as-exception vs failure-as-normal-state
l'échec = exceptionfailure = exception
rare · fatal · subirare · fatal · endured
⇄
ouor
l'échec = état normalfailure = normal state
prévu · borné · géréexpected · bounded · handled

Sur un seul process, un appel échoue rarement, et c'est presque toujours un vrai bug. Sur le réseau, l'échec transitoire est constant : à grande échelle, il y en a en permanence, et la plupart se résolvent seuls en quelques millisecondes. Le traiter comme une exception fatale, c'est faire tomber un système pour un hoquet. Le traiter comme un état normal — borné par un timeout, absorbé par un retry espacé — c'est le rendre robuste sans même réveiller un humain.On a single process, a call rarely fails, and it's almost always a real bug. On the network, transient failure is constant: at scale, there's some all the time, and most resolve themselves in milliseconds. Treating it as a fatal exception means toppling a system over a hiccup. Treating it as a normal state — bounded by a timeout, absorbed by a spaced-out retry — makes it robust without even waking a human.

Le réflexe du volumeThe volume's reflex

« Qu'est-ce qui se passe quand l'autre côté ne répond pas — ou répond deux fois ? » Trois outils répondent à la première moitié. Le timeout borne l'attente : on n'immobilise pas une ressource pour un service qui ne reviendra peut-être jamais. Le retry absorbe le transitoire : un échec passager, on le re-tente plutôt que de le propager. Le backoff espace les tentatives : on attend de plus en plus longtemps entre deux essais, pour laisser au service le temps de se relever au lieu de l'achever. Les trois ensemble transforment l'échec réseau d'événement catastrophique en routine silencieuse.'What happens when the other side doesn't answer — or answers twice?' Three tools answer the first half. The timeout bounds the wait: you don't tie up a resource for a service that may never come back. The retry absorbs the transient: a passing failure, you re-attempt rather than propagate. The backoff spaces the attempts: you wait longer and longer between tries, to give the service time to recover instead of finishing it off. The three together turn network failure from a catastrophic event into a silent routine.

« Répond deux fois » est l'autre moitié de la question.'Answers twice' is the other half of the question.

Dès qu'on introduit le retry, on crée un risque neuf : si le premier appel avait réussi mais que la réponse s'est perdue, le re-tenter exécute l'action deux fois. Débiter, envoyer un mail, créer une commande — deux fois. C'est pourquoi retry et idempotence sont indissociables : une opération qu'on peut rejouer sans dommage (une clé d'idempotence qui dé-doublonne côté serveur) est la condition pour réessayer sans danger. Ce numéro installe le retry ; l'idempotence est le garde-fou qui l'accompagne partout — on la retrouvera au cœur des prochaines marches du volume.The moment you introduce the retry, you create a new risk: if the first call succeeded but the response was lost, re-attempting runs the action twice. Charge, send an email, create an order — twice. That's why retry and idempotency are inseparable: an operation you can replay without harm (an idempotency key that de-duplicates on the server) is the precondition for retrying safely. This issue installs the retry; idempotency is the guardrail that accompanies it everywhere — we'll find it at the heart of the volume's next steps.

№ 02La douleurThe pain4 min

L'attente infinie fige tout, et le retry naïf achève le blessé.The infinite wait freezes everything, and the naive retry finishes off the wounded.

Mal gérer l'échec réseau a trois coûts qui se renforcent. Sans timeout, une dépendance lente fige tous ses appelants : les threads et les connexions s'accumulent en attendant une réponse qui ne vient pas, et c'est tout le système qui se bloque pour un seul service en peine. Avec un retry naïf — immédiat, en boucle —, on tape un service déjà au sol : tous les clients réessaient en même temps à la milliseconde où il hoquette, et la tempête de retries transforme un micro-incident en panne franche. Et sans idempotence, chaque retry qui aboutit après un premier appel réussi-mais-perdu duplique l'effet : on débite, on notifie, on crée deux fois. Trois manières de transformer un échec transitoire — qui se serait résolu seul — en incident majeur.Mishandling network failure has three costs that reinforce each other. Without a timeout, a slow dependency freezes all its callers: threads and connections pile up awaiting an answer that never comes, and the whole system locks over one struggling service. With a naive retry — immediate, in a loop —, you hammer a service already down: every client retries at the same millisecond it hiccups, and the retry storm turns a micro-incident into a hard outage. And without idempotency, every retry that lands after a first succeeded-but-lost call duplicates the effect: you charge, notify, create twice. Three ways to turn a transient failure — which would have resolved itself — into a major incident.

№ 1 · L'attente infinie fige toutThe infinite wait freezes all

Sans timeout, un appel vers un service lent ne rend jamais la main. Le thread reste bloqué, la connexion reste ouverte, et sous la charge ils s'épuisent : le pool est plein, les nouveaux appels attendent, et un service parfaitement sain tombe parce que sa dépendance, elle, ramait. La lenteur de l'un devient la panne de tous.Without a timeout, a call to a slow service never returns. The thread stays blocked, the connection stays open, and under load they exhaust: the pool is full, new calls wait, and a perfectly healthy service falls because its dependency was merely slow. One's slowness becomes everyone's outage.

№ 2 · Le retry naïf fait tempêteThe naive retry storms

Réessayer immédiatement, en boucle, au moment précis où un service faiblit, c'est le frapper plus fort quand il est le plus fragile. Tous les clients synchronisés re-tapent ensemble : la charge double, triple, et le service qui se serait relevé en 200 ms s'écroule pour de bon. C'est le thundering herd — le retry qui cause la panne qu'il prétend soigner.Retrying immediately, in a loop, at the exact moment a service weakens, means hitting it harder when it's most fragile. All synchronized clients re-hammer together: load doubles, triples, and the service that would have recovered in 200 ms collapses for good. That's the thundering herd — the retry causing the outage it claims to cure.

№ 3 · Le retry sans idempotence dupliqueRetry without idempotency duplicates

Le premier appel a débité, mais sa réponse s'est perdue dans le timeout. Le retry repart, débite encore : le client paie deux fois. Sans clé d'idempotence pour que le serveur reconnaisse « c'est la même demande », chaque effet de bord rejoué se cumule — mails en double, commandes fantômes, stock décrémenté deux fois. Le retry, censé sauver, corrompt.The first call charged, but its response was lost in the timeout. The retry sets off, charges again: the customer pays twice. Without an idempotency key for the server to recognize 'it's the same request', every replayed side effect compounds — duplicate emails, ghost orders, stock decremented twice. The retry, meant to save, corrupts.

Un retry immédiat, en boucle, sans clé — les trois douleurs d'un coupAn immediate retry, in a loop, keyless — the three pains at once
def charge(order_id, amount):
    while True:                                  # 💥 boucle sans borne ni espacement
        try:
            return http.post("http://billing/charge",
                             json={"amount": amount})   # 💥 aucune clé : rejeu = double débit
        except (Timeout, ConnError):
            continue        # 💥 on re-tape AUSSITÔT — pile quand billing est au plus mal
# pas de timeout → on peut attendre l'infini · pas de backoff → tempête ·
# pas d'idempotence → le client est débité autant de fois que la boucle tourne.
Le diagnostic.The diagnosis.

Le détecteur : « cet appel a-t-il une borne d'attente, une stratégie d'espacement entre tentatives, et le droit d'être rejoué sans dommage ? ». Si l'un des trois manque, l'appel est une bombe à retardement réseau. Le réflexe : un timeout court qui rend la main au lieu de figer ; un nombre borné de retries — pas une boucle — réservés aux échecs vraiment transitoires ; un backoff exponentiel avec jitter pour désynchroniser les clients ; et une clé d'idempotence pour que rejouer ne duplique jamais. Quatre garde-fous qui, ensemble, font de l'échec réseau un non-événement — et qui, séparés, se retournent l'un après l'autre contre le système.The detector: 'does this call have a wait bound, a spacing strategy between attempts, and the right to be replayed without harm?'. If any of the three is missing, the call is a network time bomb. The reflex: a short timeout that returns control instead of freezing; a bounded number of retries — not a loop — reserved for truly transient failures; an exponential backoff with jitter to desynchronize clients; and an idempotency key so replaying never duplicates. Four guardrails that, together, make network failure a non-event — and that, apart, turn one after another against the system.

№ 03La tempêteThe storm5 min

Le retry naïf fait la tempête — le timeout, le backoff et le jitter la calment.The naive retry storms — timeout, backoff and jitter calm it.

Les trois outils ne se comprennent qu'ensemble, en regardant la charge dans le temps. L'animation part de la tempête — quatre clients qui re-tapent en rafale synchronisée un service à terre —, puis applique chaque garde-fou : le timeout borne l'attente et arrête la boucle, le backoff espace les tentatives en allongeant les délais, le jitter les désynchronise par un peu de hasard. À chaque marche, la forêt de tentatives se clairseme, et le service passe du sol au rétablissement. Ce n'est pas un outil qui sauve, c'est leur combinaison.The three tools only make sense together, watching load over time. The animation starts from the storm — four clients re-hammering a downed service in synchronized bursts —, then applies each guardrail: the timeout bounds the wait and stops the loop, the backoff spaces attempts by lengthening delays, the jitter desynchronizes them with a bit of randomness. At each step, the forest of attempts thins, and the service goes from down to recovered. It's not one tool that saves, it's their combination.

Animation — quatre clients, une timeline, un service qui encaisseAnimation — four clients, one timeline, a service taking the hits
Départ — la tempête de retriesStart — the retry storm · 1/4
client 1client 1
client 2client 2
client 3client 3
client 4client 4
chargeload
service au solservice down
Le service hoquette, et les quatre clients re-tapent aussitôt, en boucle, tous à la même milliseconde. La charge ne retombe jamais : chaque tentative frappe un service déjà à terre. Le retry, censé soigner, est devenu la cause de la panne.The service hiccups, and the four clients re-hammer at once, in a loop, all at the same millisecond. Load never drops: each attempt strikes a service already down. The retry, meant to cure, has become the cause of the outage.

Les quatre garde-fous, ensemble, en une fonctionThe four guardrails, together, in one function
def call_with_resilience(order_id, amount, max_tries=4):
    for attempt in range(max_tries):                    # retry BORNÉ, pas une boucle
        try:
            return billing.charge(amount,
                                  idempotency_key=order_id,   # rejouable sans double débit
                                  timeout=2.0)                # borne l'attente
        except Transient:                               # on ne retente QUE le transitoire
            if attempt == max_tries - 1: raise          # dernier essai → on propage
            delay = (2 ** attempt) + random.uniform(0, 1)   # backoff exponentiel + jitter
            sleep(delay)                                #  1s · 2s · 4s … + un peu de hasard
Le motif transférable.The transferable pattern.

« Borne l'attente, ne retente que le transitoire, espace et désynchronise les tentatives, et rends l'opération rejouable. » Le même motif vaut bien au-delà des microservices : un client HTTP vers une API tierce, une reconnexion à une base, un consumer qui dépile une file, un appel SDK vers le cloud. Partout où un appel franchit une frontière faillible, ces quatre réglages décident s'il devient une panne ou un non-événement. Et l'échec qui résiste à tous les retries n'est pas perdu : il part dans une Dead Letter Queue, pour être rejoué ou inspecté plus tard plutôt que silencieusement avalé.'Bound the wait, retry only the transient, space and desynchronize the attempts, and make the operation replayable.' The same pattern holds well beyond microservices: an HTTP client to a third-party API, a database reconnection, a consumer draining a queue, an SDK call to the cloud. Anywhere a call crosses a fallible boundary, these four settings decide whether it becomes an outage or a non-event. And the failure that resists all retries isn't lost: it goes to a Dead Letter Queue, to be replayed or inspected later rather than silently swallowed.

Modèle mentalMental model
L'imageThe image

Rappeler quelqu'un au téléphone : on raccroche après quelques sonneries (timeout), on patiente un peu plus longtemps avant chaque nouvel essai (backoff), et on évite de rappeler pile à la seconde où tout le monde rappelle (jitter).Calling someone back on the phone: you hang up after a few rings (timeout), you wait a little longer before each new try (backoff), and you avoid calling back at the exact second everyone else does (jitter).

La règleThe rule

Ne retente que le transitoire et l'idempotent ; borne l'attente et le nombre d'essais ; espace-les, et surtout désynchronise-les.Retry only the transient and the idempotent; bound the wait and the attempt count; space them out, and above all desynchronize them.

Où l'analogie casseWhere the analogy breaks

Au téléphone, rappeler ne coûte rien à l'autre, et un appel n'aboutit jamais « deux fois ». Sur le réseau, chaque tentative charge un service déjà à terre — insister l'achève — et un appel qui a abouti sans que la réponse revienne s'est quand même exécuté : le rejouer débite deux fois. D'où l'idempotence, une question que le téléphone n'a jamais à se poser.On the phone, calling back costs the other side nothing, and a call never lands 'twice'. On the network, each attempt loads an already-downed service — insisting finishes it off — and a call that landed without its reply coming back still ran: replaying it charges twice. Hence idempotency, a question the phone never has to ask.

№ 04Le piègeThe trap4 min

Tout retenter n'est pas être robuste. Le retry qui amplifie la panne.Retrying everything isn't being robust. The retry that amplifies the outage.

Le piège n'est pas le retry — c'est de retenter sans discernement. Deux dérives se cumulent. D'abord on retente ce qui ne se réparera jamais : un « 400 montant invalide » ne devient pas valide au cinquième essai, un « 409 déjà traité » non plus — on s'acharne sur des échecs définitifs, on gaspille du temps et on masque le vrai bug. Ensuite, les retries s'empilent en cascade : si chaque maillon d'une chaîne d'appels retente cinq fois, un clic devient cinq, puis vingt-cinq, puis cent vingt-cinq tentatives, et une lenteur en bout de chaîne remonte en avalanche qui sature tout l'amont. Le retry n'amortit l'échec que s'il est borné, réservé au transitoire, et non répété à chaque étage. Bascule le toggle : cherche le retry qui s'acharne, et celui qui se multiplie.The trap isn't the retry — it's retrying indiscriminately. Two drifts compound. First you retry what will never heal: a '400 invalid amount' doesn't become valid on the fifth try, nor does a '409 already processed' — you hammer on definitive failures, waste time and mask the real bug. Then, retries stack in cascade: if each link of a call chain retries five times, one click becomes five, then twenty-five, then one hundred twenty-five attempts, and a slowness at the chain's end surges back as an avalanche that saturates everything upstream. The retry only cushions failure if it's bounded, reserved for the transient, and not repeated at every tier. Flip the toggle: hunt the retry that hammers, and the one that multiplies.

# on ne retente QUE ce qui est transitoire, et seulement si c'est rejouable sans dommage
def charge(order_id, amount):
    return retry(
        lambda: billing.charge(amount, idempotency_key=order_id, timeout=2.0),
        max_tries=4,
        backoff=expo_jitter,                  # 1s·2s·4s + hasard
        retry_on=[Timeout, ConnReset, HTTP503],   # transitoire : ça vaut le coup
        give_up_on=[HTTP400, HTTP409, HTTP422],   # définitif : retenter ne changera rien
    )
# 400 "montant invalide" ne deviendra jamais valide en réessayant : on échoue VITE.
# et la clé d'idempotence garantit qu'un rejeu ne débite pas deux fois.
Pourquoi c'est justeWhy it's right

On a trié. Seuls les échecs transitoires — timeout, connexion coupée, 503 — sont retentés, parce qu'eux seuls peuvent se résoudre d'eux-mêmes ; les échecs définitifs — 400, 409, 422 — font échouer vite, rendant l'erreur tout de suite au lieu de s'acharner. Le nombre de tentatives est borné, le backoff avec jitter évite la tempête, et la clé d'idempotence garantit qu'un rejeu ne débite jamais deux fois. Le retry fait exactement son travail — absorber le hoquet passager — sans déborder sur ce qu'il ne peut pas réparer. C'est la différence entre une robustesse pensée et un acharnement qui aggrave.You sorted. Only transient failures — timeout, dropped connection, 503 — are retried, because they alone can resolve themselves; definitive failures — 400, 409, 422 — fail fast, returning the error at once instead of hammering. The number of attempts is bounded, the backoff with jitter avoids the storm, and the idempotency key guarantees a replay never charges twice. The retry does exactly its job — absorb the passing hiccup — without spilling onto what it can't repair. That's the difference between thought-out robustness and hammering that worsens.

La règle qui tranche.The rule that settles it.

Ne retente que ce qui est à la fois transitoire (l'erreur peut disparaître seule) et idempotent (le rejouer ne duplique rien). Le test : « si je rejoue cet appel dix fois, est-ce que ça peut réussir un jour, et est-ce que ça reste sans dommage ? ». Deux oui : retente, borné, avec backoff et jitter. Un non sur le transitoire : échoue vite, c'est définitif. Un non sur l'idempotence : rends d'abord l'opération rejouable (une clé), sinon le retry corrompt. Et une seule couche retente : on place la résilience au bon étage, jamais à tous, pour que les facteurs ne se multiplient pas en cascade.Retry only what is both transient (the error can vanish on its own) and idempotent (replaying it duplicates nothing). The test: 'if I replay this call ten times, can it ever succeed, and does it stay harmless?'. Two yeses: retry, bounded, with backoff and jitter. A no on transient: fail fast, it's definitive. A no on idempotency: first make the operation replayable (a key), otherwise the retry corrupts. And only one layer retries: place resilience at the right tier, never at all of them, so the factors don't multiply in cascade.

Le piège jumeau : l'échec qu'on avale en silence.The twin trap: the failure you swallow in silence.

L'excès inverse du retry forcené : retenter quelques fois, échouer, et… ne rien faire. La demande disparaît sans trace, le client croit sa commande passée, et personne n'est alerté. Un retry borné doit avoir une issue : quand toutes les tentatives sont épuisées, l'échec part dans une Dead Letter Queue — une file d'attente des échecs définitifs — pour être rejoué à la main, inspecté, ou compensé, jamais silencieusement perdu. Le retry gère le transitoire ; la DLQ rattrape ce qui a résisté. Sans elle, la résilience n'est qu'un délai avant une perte invisible.The opposite excess of the frantic retry: retry a few times, fail, and… do nothing. The request vanishes without a trace, the client believes their order went through, and no one is alerted. A bounded retry must have an outcome: when all attempts are exhausted, the failure goes to a Dead Letter Queue — a queue of definitive failures — to be replayed by hand, inspected, or compensated, never silently lost. The retry handles the transient; the DLQ catches what resisted. Without it, resilience is just a delay before an invisible loss.

№ 05Bilan & éditoWrap-up & editorial2 min

L'échec qu'on subit, ou l'échec qu'on a prévu.Failure you endure, or failure you planned for.

Appel naïf vs appel résilient, côte à côteNaive call vs resilient call, side by side
appel naïfnaive callappel résilientresilient call
L'attenteThe waitinfinie · fige le poolinfinite · freezes the poolbornée par un timeoutbounded by a timeout
Le retryThe retryimmédiat, en boucleimmediate, loopingborné · backoff + jitterbounded · backoff + jitter
Quoi retenterWhat to retrytout, sans triereverything, unsortedle transitoire seulthe transient only
RejouerReplayingduplique l'effetduplicates the effectidempotent · une cléidempotent · a key
L'échec finalThe final failureavalé en silenceswallowed in silenceDead Letter QueueDead Letter Queue

Quand ne pas (sur-)retenterWhen not to (over-)retry

La résilience a un loyer : de la latence ajoutée, de la complexité, des effets à rendre rejouables. On appelle simplement, sans étage de retry — et c'est correct — quand :Resilience has rent: added latency, complexity, effects to make replayable. Just call, with no retry tier — and that's fine — when:

  • l'appel est local, en mémoire : pas de réseau, pas d'échec transitoire à absorber. Emballer un appel de fonction dans un retry, c'est de la cérémonie (YAGNI, Vol 0 #3) qui n'a aucune panne à soigner.the call is local, in-memory: no network, no transient failure to absorb. Wrapping a function call in a retry is ceremony (YAGNI, Vol 0 #3) with no failure to cure.
  • l'échec est définitif par nature : une validation, une autorisation refusée, une ressource qui n'existe pas. Retenter ne changera jamais le verdict — il faut échouer vite et rendre l'erreur, pas s'acharner.the failure is definitive by nature: a validation, a denied authorization, a resource that doesn't exist. Retrying will never change the verdict — you must fail fast and return the error, not hammer.
  • une couche en amont retente déjà : empiler un retry sur un retry multiplie les tentatives en cascade. La résilience se place à un seul étage — souvent au plus près de l'appel faillible — pas à chacun.an upstream layer already retries: stacking a retry on a retry multiplies attempts in cascade. Resilience sits at a single tier — often closest to the fallible call — not at each one.

Le réflexe n'est donc pas « mets un retry partout, c'est robuste » : c'est « cet échec est-il transitoire et l'opération rejouable sans dommage ? Alors un retry borné, espacé, avec une issue pour ce qui résiste. Sinon, échoue vite, et franchement. »So the reflex isn't 'put a retry everywhere, it's robust': it's 'is this failure transient and the operation replayable without harm? Then a bounded, spaced retry, with an outcome for what resists. Otherwise, fail fast, and plainly.'

Mot de l'éditeurFrom the editor

Ce numéro ouvre le Vol 7 par le renversement qui fonde tout le volume : l'échec transitoire n'est pas une exception rare, c'est un état normal du réseau. Sur un seul process, un appel qui échoue est presque toujours un vrai bug ; sur le fil, l'échec passager est permanent, et la plupart se résout seul en quelques millisecondes. Toute la résilience découle de ce changement de regard : on ne subit plus l'échec, on le prévoit — borné par un timeout, absorbé par un retry espacé et désynchronisé, réservé au transitoire, rendu rejouable par l'idempotence, et rattrapé par une file quand il résiste. Quatre réglages qui transforment un événement catastrophique en routine silencieuse.This issue opens Vol 7 with the reversal that founds the whole volume: transient failure isn't a rare exception, it's a normal state of the network. On a single process, a failing call is almost always a real bug; on the wire, passing failure is constant, and most resolves itself in milliseconds. All resilience flows from this shift in gaze: you no longer endure failure, you plan for it — bounded by a timeout, absorbed by a spaced and desynchronized retry, reserved for the transient, made replayable by idempotency, and caught by a queue when it resists. Four settings that turn a catastrophic event into a silent routine.

Mais le retry a une limite : si le service d'en face est vraiment mort — pas un hoquet, une panne franche —, continuer à le solliciter, même espacé, ne fait que l'empêcher de se relever et gaspiller nos propres ressources. C'est le sujet du #2, le Circuit Breaker : cesser d'appeler un service à terre pour le laisser respirer, et échouer immédiatement le temps qu'il se rétablisse. Puis le #3 affrontera l'autre moitié de la question du volume — « ou répond deux fois ? » — en regardant la vérité en face : « exactly-once » n'existe pas sur un réseau. On conçoit pour le « au moins une fois », avec l'idempotence aperçue ici et la Saga pour orchestrer ce qui ne tient plus dans une transaction. Le fil rouge : le réseau ne ment pas, il oublie, répète et tarde — et on code en le sachant.But the retry has a limit: if the service across is truly dead — not a hiccup, a hard outage —, continuing to call it, even spaced, only keeps it from recovering and wastes our own resources. That's #2's subject, the Circuit Breaker: stop calling a downed service to let it breathe, and fail immediately while it recovers. Then #3 will face the volume's other half — 'or answers twice?' — by looking the truth in the eye: 'exactly-once' doesn't exist on a network. You design for 'at least once', with the idempotency glimpsed here and the Saga to orchestrate what no longer fits in a transaction. The through-line: the network doesn't lie, it forgets, repeats and lags — and you code knowing it.

Les voisins que ce numéro débloqueThe neighbors this issue unlocks

  • Exponential backoff — la stratégie d'espacement même : doubler le délai à chaque tentative (1s, 2s, 4s, 8s) pour laisser au service le temps de se relever. Sans elle, le retry frappe à fréquence constante et entretient la surcharge ; avec elle, la pression décroît à mesure que l'échec persiste.the spacing strategy itself: double the delay at each attempt (1s, 2s, 4s, 8s) to give the service time to recover. Without it, the retry strikes at constant frequency and sustains the overload; with it, pressure decreases as the failure persists.
  • Jitter — le grain de hasard ajouté au backoff. Sans lui, tous les clients qui ont échoué ensemble réessaient ensemble — des vagues synchronisées qui re-saturent le service à chaque palier. Le jitter disperse les tentatives dans le temps et dissout le thundering herd ; c'est le détail qui fait vraiment tenir le backoff à grande échelle. Sa forme la plus efficace, le « full jitter », tire au sort tout le délai entre zéro et le palier.the grain of randomness added to the backoff. Without it, all clients that failed together retry together — synchronized waves that re-saturate the service at each step. Jitter scatters attempts across time and dissolves the thundering herd; it's the detail that truly makes backoff hold at scale. Its most effective form, 'full jitter', draws the whole delay at random between zero and the step.
  • Dead Letter Queue — l'issue de l'échec qui résiste à tous les retries. Plutôt que de le perdre en silence, on le dépose dans une file dédiée : il y sera rejoué, inspecté ou compensé, à la main ou plus tard. C'est ce qui distingue un système qui encaisse l'échec d'un système qui l'oublie — et le pont vers les files de messages (Vol 4 #2) et la Saga (#3).the outcome for the failure that resists all retries. Rather than losing it in silence, you drop it in a dedicated queue: there it will be replayed, inspected or compensated, by hand or later. It's what separates a system that absorbs failure from one that forgets it — and the bridge to message queues (Vol 4 #2) and the Saga (#3).

Le kata — écris-le, ne le relis pasThe kata — write it, don't reread it

Tu as vu les quatre garde-fous calmer la tempête au chapitre 3. Reste le geste qui ancre : les écrire. Copie ce fichier de tests dans ton langage et ton runner habituels — la spec tient dans les noms des tests — et fais-les passer au vert sans revenir au numéro. Le dernier test est le sournois : c'est lui qui vérifie que ton retry sait distinguer le transitoire (qu'on retente) du définitif (qu'on rend tout de suite) — la moitié du pattern qu'on oublie toujours.You watched the four guardrails calm the storm in chapter 3. What's left is the gesture that anchors it: writing them. Copy this test file into your usual language and runner — the spec fits in the test names — and turn them green without coming back to the issue. The last test is the sneaky one: it checks that your retry can tell the transient (which you retry) from the definitive (which you return at once) — the half of the pattern everyone forgets.

test_retry.py
# test_retry.py — la spec EST le harnais : fais-le passer au vert.
# un retry BORNÉ : il n'insiste que sur le transitoire, espace ses essais, sait s'arrêter.

def test_reussit_du_premier_coup():
    sonde = compteur(lambda: "ok")
    assert retry(sonde) == "ok"                  # aucun échec → aucun retry
    assert sonde.count == 1                       # un seul appel, transparent

def test_retente_le_transitoire_puis_reussit():
    sonde = echoue_n_fois(2, Transient, puis="ok")
    assert retry(sonde, max_tries=4) == "ok"     # 2 hoquets absorbés…
    assert sonde.count == 3                       # …le 3e essai passe

def test_borne_le_nombre_d_essais():
    sonde = compteur(dead)                        # dead lève Transient à chaque fois
    with raises(Transient): retry(sonde, max_tries=4)
    assert sonde.count == 4                        # 4 essais, pas une boucle infinie

def test_n_attend_pas_apres_le_dernier_essai():
    delays = capture_les_sleeps()
    with raises(Transient): retry(dead, max_tries=4)
    assert len(delays) == 3                        # 4 essais → 3 pauses : rien après le dernier

def test_backoff_croit_a_chaque_essai():
    delays = capture_les_sleeps()
    with raises(Transient): retry(dead, max_tries=4)
    assert delays[0] < delays[1] < delays[2]      # ~1s < ~2s < ~4s : l'espacement grandit

def test_echec_definitif_echoue_vite():
    sonde = compteur(lambda: leve(Invalid))       # 400 : verdict définitif
    with raises(Invalid): retry(sonde, max_tries=4)
    assert sonde.count == 1                        # on NE retente PAS : retenter ne le validera jamais
Chez toi, cette semaine.In your codebase, this week.

Cherche le retry qui retente tout — le piège du chapitre 4. Un point de départ : grep -rn "except\b" src/ | grep -iE "retry|attempt|for _ in range", puis regarde le except juste au-dessus de chaque boucle. S'il attrape Exception (ou rien de précis), il retente aussi les 400, 409 et 422 qui ne guériront jamais. La question à poser à chacun : « cet échec peut-il disparaître seul, et le rejouer reste-t-il sans dommage ? » Un seul non, et le retry n'a rien à faire là.Find the retry that retries everything — chapter 4's trap. A starting point: grep -rn "except\b" src/ | grep -iE "retry|attempt|for _ in range", then look at the except just above each loop. If it catches Exception (or nothing precise), it also retries the 400s, 409s and 422s that will never heal. The question for each: 'can this failure vanish on its own, and does replaying it stay harmless?' A single no, and the retry has no business there.