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

Circuit Breaker, ou le disjoncteur qui coupe pour laisser revenirCircuit Breaker, or the switch that cuts to let things come back

Le #1 a appris à retenter le hoquet ; ce numéro affronte son angle mort — la panne franche et durable. Quand le service d'en face est vraiment mort plusieurs minutes, le retry seul devient nuisible : chaque requête paie le plein timeout, les pools saturent, et en martelant le service à terre on l'empêche de redémarrer. Le réflexe : un disjoncteur, machine à trois états — fermé, ouvert (on coupe le temps d'un cooldown), demi-ouvert (un appel test sonde le retour). Retry et disjoncteur ne se remplacent pas : ils répondent au transitoire et au soutenu, on les empile. La règle : un disjoncteur n'existe que s'il sait se refermer.Issue #1 taught retrying the hiccup; this one faces its blind spot — the hard, lasting outage. When the service across is truly dead for minutes, the retry alone turns harmful: every request pays the full timeout, pools saturate, and by hammering the downed service you keep it from restarting. The reflex: a circuit breaker, a three-state machine — closed, open (it cuts for a cooldown), half-open (a test call probes the return). Retry and breaker don't replace each other: they answer the transient and the sustained, you stack them. The rule: a breaker only exists if it can close again.

AudienceAudience
Dev dont le retry seul fait tomber tout l'amont quand une dépendance meurtDev whose retry-alone topples everything upstream when a dependency dies
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026June 2026
≈ 18 min●●●○Résilience distribuéeCircuit BreakerTradeoff
SommaireContents ·
№ 01CadrageFraming3 min

Le service n'est pas lent — il est mort. À quoi bon continuer d'appeler ?The service isn't slow — it's dead. Why keep calling at all?

Le #1 a réglé le hoquet : un échec passager, on le re-tente, espacé, désynchronisé. Mais le retry a un angle mort — la panne franche et durable. Quand le service d'en face est vraiment mort (un crash, une base saturée, un déploiement qui a mal tourné), chaque requête paie le plein tarif de l'échec : tout le timeout, toutes les tentatives, pour un échec qu'on connaissait d'avance. On gaspille nos propres ressources sur des appels condamnés, on remplit nos pools d'attentes inutiles — et notre service tombe à son tour, à cause d'une dépendance morte. Pire : en continuant de marteler le service à terre, on l'empêche de se relever. Le réflexe : un disjoncteur. Après trop d'échecs d'affilée, il s'ouvre et cesse d'appeler — il échoue immédiatement, sans même essayer —, laisse le service respirer un temps, puis teste prudemment s'il est revenu.Issue #1 settled the hiccup: a passing failure, you re-attempt it, spaced, desynchronized. But the retry has a blind spot — the hard, lasting outage. When the service across is truly dead (a crash, a saturated database, a botched deploy), every request pays the full price of failure: the whole timeout, every attempt, for a failure you knew in advance. You waste your own resources on doomed calls, fill your pools with useless waits — and your service falls in turn, because of a dead dependency. Worse: by hammering the downed service, you keep it from recovering. The reflex: a circuit breaker. After too many failures in a row, it opens and stops calling — it fails immediately, without even trying —, lets the service breathe a while, then cautiously tests whether it's back.

Le code résilient du #1 — parfait contre le hoquet, impuissant face à la mortThe resilient code from #1 — perfect against the hiccup, powerless against death
# le #1 nous a appris à retenter le transitoire. mais que se passe-t-il
# quand billing n'est pas en hoquet — quand il est franchement MORT, plusieurs minutes ?
def charge(order_id, amount):
    return retry(                                    # ✓ parfait contre un hoquet d'1s
        lambda: billing.charge(amount, idempotency_key=order_id, timeout=2.0),
        max_tries=4, backoff=expo_jitter,
    )
# billing est down. chaque requête : 4 × 2s de timeout + 1 + 2 + 4s de backoff ≈ 15 à 18s,
# pour un échec GARANTI qu'on connaissait dès la première tentative.
# pire : on continue de marteler un service à terre, et on l'empêche de se relever.
# le retry soigne le transitoire. il est impuissant — et nuisible — face à la panne franche.
Insister sur un mort vs cesser d'appelerHammering a corpse vs ceasing to call
réessayer un service mortretry a dead service
on attend · on insiste · on s'épuisewe wait · we insist · we exhaust
⇄
ouor
ouvrir le circuitopen the circuit
on échoue vite · on le laisse respirerwe fail fast · we let it breathe

Le retry parie que l'échec est passager. Quand ce pari est faux — que le service est mort pour plusieurs minutes —, insister ne fait que nous épuiser et l'achever. Le disjoncteur prend le pari inverse : après assez d'échecs, il considère le service hors-service et coupe le courant. Échouer immédiatement, sans appeler, protège deux fois — nos ressources, qu'on ne gaspille plus en attentes condamnées ; et le service à terre, à qui on offre enfin le silence dont il a besoin pour redémarrer.The retry bets failure is passing. When that bet is wrong — the service dead for several minutes — insisting only exhausts us and finishes it off. The circuit breaker takes the opposite bet: after enough failures, it deems the service out of order and cuts the power. Failing immediately, without calling, protects twice — our resources, no longer wasted on doomed waits; and the downed service, finally offered the silence it needs to restart.

Le réflexe — trois états, un seuilThe reflex — three states, one threshold

Le disjoncteur emprunte son nom et son geste à l'électricité. Au repos il est fermé (closed) : le courant passe, les appels partent normalement. S'il compte trop d'échecs d'affilée, il saute — il s'ouvre (open) : tout appel échoue instantanément, sans toucher le service, le temps d'un cooldown. Le cooldown écoulé, il passe en demi-ouvert (half-open) : il laisse passer un seul appel test. Réussi, il se referme et le trafic reprend ; échoué, il se rouvre aussitôt pour un nouveau répit. Trois états, une transition pilotée par le compte d'échecs : voilà tout le mécanisme.The breaker borrows its name and gesture from electricity. At rest it's closed: current flows, calls go out normally. If it counts too many failures in a row, it trips — it opens: every call fails instantly, without touching the service, for a cooldown. Once the cooldown elapses, it goes half-open: it lets a single test call through. Succeeded, it closes again and traffic resumes; failed, it reopens at once for another respite. Three states, one transition driven by the failure count: that's the whole mechanism.

Deux étages complémentaires, pas concurrents.Two complementary tiers, not rivals.

Retry et disjoncteur ne se remplacent pas : ils répondent à deux durées d'échec. Le retry absorbe le transitoire — la panne d'une milliseconde à quelques secondes, qui se résout seule. Le disjoncteur protège du soutenu — la panne de plusieurs minutes, qui ne se résoudra pas en réessayant. On les empile : à l'intérieur du disjoncteur, un appel peut retenter un hoquet ; mais si les échecs s'accumulent malgré les retries, c'est le signe que ce n'est plus un hoquet — le disjoncteur saute et arrête les frais. Le retry gère l'exception passagère ; le disjoncteur gère l'état durablement cassé.Retry and breaker don't replace each other: they answer two durations of failure. The retry absorbs the transient — the millisecond-to-seconds outage that resolves itself. The breaker protects against the sustained — the minutes-long outage that won't heal by retrying. You stack them: inside the breaker, a call may retry a hiccup; but if failures pile up despite the retries, that's the sign it's no longer a hiccup — the breaker trips and stops the bleeding. The retry handles the passing exception; the breaker handles the durably broken state.

№ 02La douleurThe pain4 min

Marteler un mort coûte trois fois : on se ruine, on entraîne les autres, on l'achève.Hammering a corpse costs thrice: you ruin yourself, drag others down, finish it off.

Sans disjoncteur, face à une panne durable, le retry seul a trois coûts qui s'enchaînent. D'abord chaque requête paie le plein tarif de l'échec — tout le timeout, toutes les tentatives — pour un verdict connu d'avance : on brûle du temps et des ressources en pure perte. Ensuite ces appels condamnés s'accumulent dans nos pools, qui se remplissent d'attentes inutiles : notre propre service ralentit, puis tombe, et la panne d'une dépendance en bout de chaîne remonte jusqu'à nous — c'est la défaillance en cascade. Enfin, en continuant de frapper le service à terre, on l'empêche de redémarrer : il a besoin de silence pour se relever, on lui envoie une tempête. Trois manières de transformer la panne d'un seul service en panne de tout le système.Without a breaker, facing a lasting outage, the retry alone has three costs that chain together. First, every request pays the full price of failure — the whole timeout, every attempt — for a verdict known in advance: you burn time and resources for nothing. Then these doomed calls pile up in your pools, which fill with useless waits: your own service slows, then falls, and a dependency's outage at the chain's end surges back to you — that's cascading failure. Finally, by hitting the downed service, you keep it from restarting: it needs silence to recover, you send it a storm. Three ways to turn one service's outage into the whole system's.

№ 1 · On paie le plein tarif d'un échec connuYou pay full price for a known failure

Le service est mort, on le sait dès la première tentative — mais sans disjoncteur, chaque requête refait tout le parcours : le timeout complet, multiplié par les retries. Huit, dix, quatorze secondes d'attente, pour un échec garanti. Sur du trafic soutenu, c'est une hémorragie de temps et de ressources dépensés à confirmer une mauvaise nouvelle qu'on connaissait déjà.The service is dead, you know it from the first attempt — but without a breaker, every request redoes the whole journey: the full timeout, times the retries. Eight, ten, fourteen seconds of waiting, for a guaranteed failure. Under steady traffic, it's a hemorrhage of time and resources spent confirming bad news you already knew.

№ 2 · La panne remonte en cascadeThe outage cascades upward

Chaque appel condamné occupe un thread et une connexion pendant tout le timeout. Sous la charge, le pool de l'appelant se remplit de ces attentes mortes : il ne peut plus servir personne, ralentit, puis tombe. Son propre appelant subit alors la même chose, et ainsi de suite. La panne d'un service en bout de chaîne se propage vers l'amont et fait tomber des services parfaitement sains.Each doomed call holds a thread and a connection for the whole timeout. Under load, the caller's pool fills with these dead waits: it can no longer serve anyone, slows, then falls. Its own caller then suffers the same, and so on. One end-of-chain service's outage propagates upstream and topples perfectly healthy services.

№ 3 · On empêche le service de se releverYou keep the service from recovering

Un service tombé essaie de redémarrer : vider une file, reconstruire un cache, rétablir des connexions. Pour ça, il lui faut un répit. En continuant de le marteler dès qu'il esquisse un retour, on le re-sature à l'instant précis où il était le plus fragile — et il retombe. L'insistance transforme une panne de deux minutes en panne de vingt : on soigne par le silence, pas par l'acharnement.A downed service tries to restart: draining a queue, rebuilding a cache, re-establishing connections. For that, it needs respite. By hammering it the moment it attempts a comeback, you re-saturate it at the exact instant it was most fragile — and it falls again. The insistence turns a two-minute outage into a twenty-minute one: you heal by silence, not by hammering.

Du retry seul à la cascade — le coût d'une panne franche sans disjoncteurFrom retry alone to cascade — the cost of a hard outage without a breaker
# billing est mort depuis 5 minutes. à chaque commande :
def checkout(order):
    reserve_stock(order)                 # ✓ ok
    status = charge(order.id, order.total)   # 💥 retry × 4 × timeout 2s ≈ 8s d'attente…
    #                                          …pour un échec qu'on connaissait à la 1re ms
    notify(order)
# 200 commandes/min × 8s bloquées = le pool de checkout sature en moins d'une minute.
# checkout tombe → le panier tombe → la home tombe. billing a éternué, tout le site est par terre.
# et billing, qu'on bombarde de retries, n'a jamais le silence pour redémarrer.
Le diagnostic.The diagnosis.

Le détecteur : « quand cette dépendance est durablement morte, qu'est-ce qui m'empêche de continuer à l'appeler en vain ? ». Si la réponse est « rien — on retente jusqu'à ce que ça revienne », alors un seul service en panne peut faire tomber tout l'amont, et l'acharnement l'empêche de revenir. Le réflexe : un disjoncteur qui compte les échecs, et qui, passé un seuil, coupe le courant — il échoue immédiatement sans appeler, pendant un cooldown, puis teste prudemment le retour avec un seul appel. On échoue vite au lieu d'attendre ; on protège nos ressources au lieu de les noyer ; et on offre au service le silence qui le laisse se relever.The detector: 'when this dependency is durably dead, what stops me from calling it in vain?'. If the answer is 'nothing — we retry until it comes back', then one downed service can topple everything upstream, and the hammering keeps it from coming back. The reflex: a breaker that counts failures and, past a threshold, cuts the power — it fails immediately without calling, for a cooldown, then cautiously tests the return with a single call. You fail fast instead of waiting; you protect your resources instead of drowning them; and you offer the service the silence that lets it recover.

№ 03Le disjoncteurThe breaker5 min

Fermé, ouvert, demi-ouvert — la boucle qui s'ouvre puis se répare seule.Closed, open, half-open — the loop that opens, then heals itself.

Le disjoncteur est une petite machine à états — trois états, des transitions pilotées par un compteur d'échecs. L'animation la fait tourner : on part fermé (le courant passe), le service tombe et les échecs montent jusqu'au seuil, le disjoncteur s'ouvre et coupe tout, il laisse le service respirer le temps d'un cooldown, puis teste le retour avec un seul appel en demi-ouvert, et se referme dès que ce test réussit. Suis le compteur d'échecs et l'état du service en aval : c'est leur danse qui décide quand couper et quand rouvrir.The breaker is a small state machine — three states, transitions driven by a failure counter. The animation runs it: we start closed (current flows), the service falls and failures climb to the threshold, the breaker opens and cuts everything, it lets the service breathe for a cooldown, then tests the return with a single call in half-open, and closes again as soon as that test succeeds. Follow the failure counter and the downstream service's state: it's their dance that decides when to cut and when to reopen.

Animation — la machine à états du disjoncteurAnimation — the breaker's state machine
Fermé — le courant passeClosed — current flows · 1/5
FERMÉCLOSED→OUVERTOPEN→DEMI-OUVERTHALF-OPEN
échecs consécutifsconsecutive failures
0/5
appelantcaller──→billing · vivantalive
Au repos, le disjoncteur est fermé : chaque appel part vers le service, comme s'il n'existait pas. Il se contente de compter — succès, échec —, et tant que les échecs restent rares, il ne fait rien d'autre que laisser passer. C'est l'état normal, le coût zéro.At rest, the breaker is closed: every call goes to the service as if it weren't there. It merely counts — success, failure —, and as long as failures stay rare, it does nothing but let calls through. That's the normal state, the zero-cost one.

La même machine, en une fonction — trois lignes à retrouverThe same machine, in one function — three lines to recall
class CircuitBreaker:
    def __init__(self, threshold=5, cooldown=30):
        self.state, self.fails, self.opened_at = "closed", 0, None

    def call(self, fn):
        if self.state == "open":
            if now() - self.opened_at < self.cooldown:            self.state = "half"                  # cooldown fini → on teste

        try:
            result = fn()                        # fermé ou demi-ouvert : on tente            return result
        except Transient:
            self.fails += 1            raise
3 ligne(s) à retrouver — écris-la de tête avant de cliquer3 line(s) to recall — write it in your head before clicking
Les trois réglages qui font tout.The three settings that decide everything.

Un disjoncteur tient en trois nombres, et chacun est un compromis. Le seuil : trop bas, il saute sur du bruit normal et coupe un service sain ; trop haut, il laisse passer la cascade avant de réagir. Le cooldown : trop court, il rouvre sur un service encore à terre et relance la tempête ; trop long, il prive le système d'un service déjà rétabli. Le demi-ouvert : l'appel test unique qui sonde sans risquer la rechute — sans lui, le disjoncteur reste ouvert pour toujours ou rouvre en grand d'un coup. Régler un disjoncteur, c'est accorder ces trois valeurs au rythme réel des pannes du service qu'il protège.A breaker comes down to three numbers, and each is a trade-off. The threshold: too low, it trips on normal noise and cuts a healthy service; too high, it lets the cascade through before reacting. The cooldown: too short, it reopens onto a still-downed service and relaunches the storm; too long, it deprives the system of an already-recovered service. The half-open: the single test call that probes without risking relapse — without it, the breaker stays open forever or swings wide open at once. Tuning a breaker means matching these three values to the real failure rhythm of the service it protects.

Bac à sable — règle le disjoncteur, puis casse-leSandbox — tune the breaker, then break it

Ce qui précède se lit ; ce qui suit se constate. Les quatre presets sont des réglages déjà cassés — un clic, et tu vois la faute se produire. Puis pousse les curseurs toi-même pendant que ça tourne : la simulation ne repart pas de zéro, elle encaisse ton nouveau réglage en marche. La règle du jeu : fais tomber la p99 sans jeter une seule requête de plus sur le service à terre.What comes before is read; what comes next is witnessed. The four presets are settings already broken — one click and you watch the mistake happen. Then push the sliders yourself while it runs: the simulation doesn't restart, it absorbs your new setting on the fly. The rule of the game: bring p99 down without throwing a single extra request at the downed service.

⚙ Le disjoncteur, en vraiThe breaker, for real
Réglage libre — pousse un curseur pendant que ça tourne, la simulation ne repart pas de zéro.Free tuning — push a slider while it runs, the simulation doesn't restart.
latence par requête (0 → 2 s)latency per request (0 → 2 s)passepasseséchoue (payé)fails (paid for)coupé (gratuit)cut (free)appel testtest call
▬ circuit (fermé / ouvert / test)circuit (closed / open / probe)▬ service en aval (vivant / mort)downstream service (alive / dead)
latence p99p99 latency
0 ms
requêtes jetées sur le service mortrequests thrown at the dead service
0
taux d'échecfailure rate
0 %
temps circuit coupétime circuit cut
0 %
faux déclenchements (cumul)false trips (total)
0
Lance la simulation pour voir le verdict de ton réglage.Run the simulation to see your setting's verdict.

Le schéma à retenir — la machine à états, figéeThe diagram to remember — the state machine, frozen
seuil atteintthreshold reached
cooldown écoulécooldown elapsed
test ratétest failed
test réussitest passed
FERMÉCLOSED
OUVERTOPEN
DEMI-OUVERTHALF-OPEN
Modèle mentalMental model
L'imageThe image

Un disjoncteur électrique : trop de courant, il saute — et c'est la coupure elle-même qui protège l'installation.An electrical circuit breaker: too much current, it trips — and the cut itself is what protects the installation.

La règleThe rule

Après N échecs consécutifs, échouer immédiatement sans appeler ; un cooldown plus tard, sonder le retour avec un seul appel test.After N consecutive failures, fail immediately without calling; one cooldown later, probe the return with a single test call.

Où l'analogie casseWhere the analogy breaks

Le disjoncteur électrique attend qu'une main le réarme. Le vôtre doit se réarmer seul — c'est le demi-ouvert. Un « disjoncteur » sans demi-ouvert n'est pas un disjoncteur : c'est un interrupteur oublié en position éteinte.The electrical breaker waits for a hand to rearm it. Yours must rearm itself — that's the half-open. A “breaker” without half-open isn't a breaker: it's a switch left stuck in the off position.

№ 04Le piègeThe trap4 min

Pas de disjoncteur, ou un disjoncteur qui ne se referme jamais.No breaker, or a breaker that never closes again.

Deux échecs symétriques encadrent le bon usage. À gauche, l'absence : on s'en remet au seul retry, et face à une panne durable on retente sans relâche, requête après requête — chacune paie le plein timeout, les pools saturent, la cascade monte. À droite, le faux ami : un disjoncteur présent mais mal réglé, qui annule son propre effet. Un seuil si haut qu'il ne saute jamais protège aussi peu que pas de disjoncteur du tout. Un cooldown d'une heure prive le système d'un service revenu en trente secondes. Et sans demi-ouvert, une fois ouvert il ne se referme jamais seul : on a transformé une panne de deux minutes en panne permanente, de nos propres mains. Bascule le toggle : repère l'acharnement sans garde-fou, et le garde-fou qui se retourne en panne.Two symmetric failures frame correct use. On the left, the absence: you rely on the retry alone, and against a lasting outage you retry relentlessly, request after request — each one pays the full timeout, pools saturate, the cascade climbs. On the right, the false friend: a breaker present but mistuned, that cancels its own effect. A threshold so high it never trips protects as little as no breaker at all. A one-hour cooldown deprives the system of a service back in thirty seconds. And without half-open, once open it never closes by itself: you've turned a two-minute outage into a permanent one, by your own hand. Flip the toggle: spot the hammering with no guardrail, and the guardrail that turns into an outage.

# disjoncteur AUTOUR de l'appel, retry À L'INTÉRIEUR — deux étages pour deux durées d'échec
breaker = CircuitBreaker(threshold=5, cooldown=30)   # 5 échecs d'affilée → on ouvre 30s

def charge(order_id, amount):
    try:
        return breaker.call(lambda: retry(           # retry = le hoquet · breaker = la panne
            lambda: billing.charge(amount, idempotency_key=order_id, timeout=2.0),
            max_tries=3, backoff=expo_jitter,
        ))
    except CircuitOpen:
        return queue_for_later(order_id, amount)      # ouvert → fallback : on diffère, sans bloquer
# panne franche : au 5e échec le circuit s'ouvre. les requêtes suivantes échouent en <1ms,
# nos pools restent libres, billing a 30s de silence pour redémarrer. au bout des 30s,
# UN appel test sonde le retour ; s'il passe, on referme. l'échec reste local et borné.
Pourquoi c'est justeWhy it's right

Les deux étages sont à leur place et chacun fait son métier. Le retry, à l'intérieur, absorbe le hoquet d'une seconde ; le disjoncteur, autour, surveille l'accumulation et coupe quand le hoquet devient panne. Le fallback donne une issue propre à l'état ouvert — on diffère la commande au lieu de la perdre ou de bloquer l'utilisateur. Les trois réglages sont accordés au service : un seuil qui distingue le bruit de la vraie panne, un cooldown assez long pour laisser respirer mais assez court pour ne pas pénaliser un retour rapide, un demi-ouvert qui sonde sans risquer la rechute. Résultat : un échec en aval reste un échec local, borné, qui ne remonte jamais en cascade — et le service mort retrouve le silence dont il a besoin.Both tiers are in place and each does its job. The retry, inside, absorbs the one-second hiccup; the breaker, around, watches the accumulation and cuts when the hiccup becomes an outage. The fallback gives a clean outcome to the open state — you defer the order instead of losing it or blocking the user. The three settings are matched to the service: a threshold that separates noise from real outage, a cooldown long enough to let it breathe but short enough not to penalize a fast return, a half-open that probes without risking relapse. Result: a downstream failure stays a local, bounded failure that never cascades upward — and the dead service regains the silence it needs.

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

Le disjoncteur protège du soutenu, le retry absorbe le transitoire — on empile les deux, on n'en choisit pas un. Le test : « si cette dépendance reste morte dix minutes, qu'est-ce qui arrête mes appels avant qu'ils ne me fassent tomber ? ». S'il n'y a pas de réponse, il manque un disjoncteur. Et un disjoncteur n'existe vraiment que s'il sait se refermer : seuil pour décider quand couper, cooldown pour laisser respirer, demi-ouvert pour constater le retour. Un disjoncteur sans demi-ouvert n'est pas un disjoncteur, c'est un interrupteur qu'on a oublié en position éteinte.The breaker protects against the sustained, the retry absorbs the transient — you stack both, you don't pick one. The test: 'if this dependency stays dead ten minutes, what stops my calls before they topple me?'. If there's no answer, a breaker is missing. And a breaker only truly exists if it can close again: threshold to decide when to cut, cooldown to let it breathe, half-open to notice the return. A breaker without half-open isn't a breaker, it's a switch left stuck in the off position.

Les voisins : isoler avant de couper.The neighbors: isolate before cutting.

Le disjoncteur coupe une dépendance morte ; deux voisins agissent en amont, pour qu'une dépendance malade n'emporte pas le reste. Le bulkhead (cloison) compartimente les ressources : un pool de connexions dédié par dépendance, pour qu'un service lent ne vide pas le pool des autres — la panne reste dans son caisson. Le rate limiter borne le débit entrant : on refuse l'excédent à la porte plutôt que de l'accepter et de s'écrouler dessous. Disjoncteur, cloison, limiteur répondent à la même peur — qu'une partie défaillante n'emporte le tout — par trois gestes : couper en aval, compartimenter au milieu, freiner à l'entrée.The breaker cuts a dead dependency; two neighbors act upstream, so a sick dependency doesn't take the rest down. The bulkhead compartmentalizes resources: a dedicated connection pool per dependency, so a slow service doesn't drain everyone else's pool — the outage stays in its compartment. The rate limiter bounds the incoming throughput: you refuse the excess at the door rather than accept it and collapse under it. Breaker, bulkhead, limiter answer the same fear — that a failing part takes the whole down — with three gestures: cut downstream, compartmentalize in the middle, throttle at the entrance.

№ 05Bilan & éditoWrap-up & editorial2 min

Insister sur un mort, ou couper pour le laisser revenir.Insisting on a corpse, or cutting to let it come back.

Retry seul vs retry + disjoncteur, côte à côteRetry alone vs retry + breaker, side by side
retry seulretry aloneretry + disjoncteurretry + breaker
Le hoquet (1s)The hiccup (1s)absorbé · c'est son métierabsorbed · that's its jobabsorbé par le retryabsorbed by the retry
La panne francheThe hard outageon s'acharne, requête après requêtehammered, request after requestcircuit ouvert · on coupecircuit open · cut off
Coût d'un échec connuCost of a known failureplein timeout × tentativesfull timeout × attemptséchec immédiat (<1ms)immediate fail (<1ms)
L'amontUpstreamsaturé · cascadesaturated · cascadeprotégé · échec localprotected · local failure
Le service à terreThe downed servicebombardé · ne revient pasbombarded · won't returnsilence · cooldown · revientsilence · cooldown · returns
Le retourThe returnpar chance, jamais testéby luck, never testedsondé en demi-ouvertprobed in half-open

Quand ne pas mettre de disjoncteurWhen not to add a breaker

Le disjoncteur a un loyer : un état partagé à maintenir, trois réglages à accorder, et le risque de couper un service sain s'il est mal réglé. On appelle sans disjoncteur — et c'est correct — quand :The breaker has rent: a shared state to maintain, three settings to tune, and the risk of cutting a healthy service if mistuned. You call without a breaker — and that's fine — when:

  • l'appel est local, en mémoire : il n'y a pas de dépendance qui puisse « mourir plusieurs minutes » à isoler. Un disjoncteur autour d'un appel de fonction protège d'une panne qui ne peut pas arriver — c'est de la cérémonie (YAGNI, Vol 0 #3).the call is local, in-memory: there's no dependency that can 'die for several minutes' to isolate. A breaker around a function call guards against an outage that can't happen — pure ceremony (YAGNI, Vol 0 #3).
  • l'échec est déjà rapide et sans effet de bord : si l'appel échoue en quelques millisecondes sans bloquer de ressource ni se propager, il n'y a ni attente à borner ni cascade à craindre. Le disjoncteur protège d'un coût qui n'existe pas.the failure is already fast and side-effect-free: if the call fails in a few milliseconds without tying up a resource or propagating, there's no wait to bound and no cascade to fear. The breaker guards against a cost that doesn't exist.
  • les appels sont trop rares pour qu'un seuil ait un sens : un job qui s'exécute une fois par heure n'accumulera jamais « cinq échecs d'affilée » de façon significative. Le compteur n'a pas de quoi se nourrir ; un simple retry avec alerte suffit.the calls are too rare for a threshold to mean anything: a job running once an hour will never meaningfully accumulate 'five failures in a row'. The counter has nothing to feed on; a simple retry with an alert is enough.

Le réflexe n'est donc pas « mets un disjoncteur sur chaque appel » : c'est « cette dépendance peut-elle rester durablement morte, et son agonie peut-elle me faire tomber ? Alors un disjoncteur, accordé à son rythme de panne, avec un fallback pour l'état ouvert. Sinon, le retry seul suffit. »So the reflex isn't 'put a breaker on every call': it's 'can this dependency stay durably dead, and can its agony topple me? Then a breaker, tuned to its failure rhythm, with a fallback for the open state. Otherwise, the retry alone is enough.'

Mot de l'éditeurFrom the editor

Le #1 traitait l'échec passager ; ce numéro traite l'échec qui dure. C'est la même question — « l'autre côté ne répond pas » — mais une autre échelle de temps, et donc un autre geste. Le retry insiste parce qu'il parie sur le retour imminent ; le disjoncteur renonce, justement parce qu'insister contre une panne durable se retourne en panne. La leçon profonde du couple, c'est que la résilience n'est pas une vertu unique : c'est un dosage entre persévérer et abandonner, accordé à la durée probable de l'échec. Trop d'insistance noie le système et achève le service ; trop d'abandon coupe un service qui revenait. Le disjoncteur encode ce dosage dans une machine à trois états qui sait, seule, quand couper et quand retenter.Issue #1 handled the passing failure; this one handles the lasting one. It's the same question — 'the other side doesn't answer' — but another time scale, and so another gesture. The retry insists because it bets on imminent return; the breaker gives up, precisely because insisting against a lasting outage turns into an outage. The pair's deep lesson is that resilience isn't a single virtue: it's a dosage between persevering and giving up, tuned to the failure's likely duration. Too much insistence drowns the system and finishes the service; too much surrender cuts a service that was coming back. The breaker encodes that dosage into a three-state machine that knows, on its own, when to cut and when to retry.

Restent deux fils que ces deux numéros ont noués sans les serrer. Le premier : le disjoncteur, comme la State machine du Vol 3, est une machine à états — trois états, des transitions gardées, des états illégaux rendus impossibles. La résilience, sous le capot, c'est de la modélisation d'états. Le second nous emmène au #3. On a installé le retry, et on a vu son ombre revenir à chaque chapitre : rejouer un appel, c'est risquer de l'exécuter deux fois. C'est l'autre moitié de la question du volume — « ou répond deux fois ? ». Le #3 la regarde en face : « exactly-once » n'existe pas sur un réseau. On conçoit pour le « au moins une fois », avec l'idempotence pour que rejouer ne duplique rien, et la Saga pour rattraper, par compensation, ce qui ne tient plus dans une transaction unique. Le fil rouge du volume tient toujours : le réseau oublie, répète et tarde — et on code en le sachant.Two threads remain that these two issues tied without tightening. The first: the breaker, like Vol 3's State machine, is a state machine — three states, guarded transitions, illegal states made impossible. Resilience, under the hood, is state modeling. The second takes us to #3. We installed the retry, and watched its shadow return every chapter: replaying a call risks running it twice. That's the volume's other half — 'or answers twice?'. Issue #3 faces it head-on: 'exactly-once' doesn't exist on a network. You design for 'at least once', with idempotency so replaying duplicates nothing, and the Saga to catch, by compensation, what no longer fits in a single transaction. The volume's through-line holds: the network forgets, repeats and lags — and you code knowing it.

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

  • Bulkhead — la cloison étanche, empruntée à la coque des navires. On cloisonne les ressources par dépendance — un pool de connexions dédié à chacune — pour qu'un service lent ne noie pas le pool commun et n'emporte pas les appels vers les services sains. Là où le disjoncteur coupe une dépendance morte, le bulkhead empêche une dépendance malade de contaminer les autres : la panne reste dans son caisson.the watertight compartment, borrowed from ships' hulls. You partition resources per dependency — a dedicated connection pool for each — so a slow service doesn't drown the shared pool and take down calls to healthy services. Where the breaker cuts a dead dependency, the bulkhead stops a sick dependency from contaminating the others: the outage stays in its compartment.
  • Rate limiter — le garde-fou de l'entrée. Plutôt que d'accepter tout le trafic et de s'écrouler sous la charge, on borne le débit : au-delà d'un seuil, on refuse (ou on diffère) à la porte. Le disjoncteur protège de ce qu'on appelle ; le rate limiter protège de ce qui nous appelle. Ensemble, ils bornent la pression dans les deux sens.the entrance guardrail. Rather than accept all traffic and collapse under load, you bound the throughput: past a threshold, you refuse (or defer) at the door. The breaker protects from what you call; the rate limiter protects from what calls you. Together, they bound pressure in both directions.
  • Fallback — l'issue de l'état ouvert. Quand le disjoncteur a coupé, l'appel doit rendre quelque chose : une valeur par défaut, une réponse en cache, une mise en file pour plus tard. Le fallback transforme « le service est mort » en dégradation gracieuse plutôt qu'en erreur brutale — c'est lui qui fait la différence entre un système qui s'effondre et un système qui rétrécit ses fonctions sans tomber.the open state's outcome. When the breaker has cut, the call must return something: a default value, a cached response, a queue for later. The fallback turns 'the service is dead' into graceful degradation rather than a brutal error — it's what separates a system that collapses from one that shrinks its features without falling.

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

Tu as vu la machine tourner et retrouvé ses trois transitions au chapitre 3. Reste le geste qui ancre : l'écrire. Deux façons, au choix. Emporte ce fichier de tests dans ton langage et ton runner habituels — la traduction est triviale, la spec tient dans les noms des tests. Ou écris-le tout de suite dans l'éditeur qui suit, en JavaScript : les six mêmes tests, exécutés dans ton navigateur, verts ou rouges en une seconde. Le dernier est le sournois : c'est lui qui vérifie que ton compteur compte des échecs consécutifs, pas des échecs cumulés.You watched the machine run and recalled its three transitions in chapter 3. What's left is the gesture that anchors it: writing it. Two ways, your pick. Take this test file into your usual language and runner — the translation is trivial, the spec fits in the test names. Or write it right now in the editor below, in JavaScript: the same six tests, run in your browser, green or red within a second. The last one is the sneaky one: it's what checks that your counter counts consecutive failures, not cumulative ones.

test_circuit_breaker.py
# test_circuit_breaker.py — la spec EST le harnais : fais-le passer au vert.
# trois états, un seuil, un cooldown, un demi-ouvert. sans lib, sans framework.

def test_ferme_laisse_passer():
    b = CircuitBreaker(threshold=3, cooldown=30)
    assert b.call(lambda: "ok") == "ok"          # au repos, transparent

def test_s_ouvre_au_seuil():
    b = CircuitBreaker(threshold=3, cooldown=30)
    for _ in range(3):
        with raises(Transient): b.call(dead)     # 3 échecs d'affilée…
    with raises(CircuitOpen): b.call(dead)       # …le 4e échoue SANS appeler

def test_ouvert_n_appelle_jamais():
    b = breaker_ouvert(threshold=3, cooldown=30)
    sonde = compteur_d_appels(dead)
    with raises(CircuitOpen): b.call(sonde)
    assert sonde.count == 0                      # zéro requête vers le service

def test_demi_ouvert_referme_sur_succes():
    b = breaker_ouvert(threshold=3, cooldown=30)
    avance_le_temps(31)                          # cooldown écoulé
    assert b.call(lambda: "ok") == "ok"          # UN appel test passe…
    assert b.state == "closed"                   # …et referme le circuit

def test_test_rate_rouvre_aussitot():
    b = breaker_ouvert(threshold=3, cooldown=30)
    avance_le_temps(31)
    with raises(Transient): b.call(dead)         # le test échoue…
    with raises(CircuitOpen): b.call(dead)       # …on rouvre, pas de 2e chance

def test_succes_remet_le_compteur_a_zero():
    b = CircuitBreaker(threshold=3, cooldown=30)
    for _ in range(2):
        with raises(Transient): b.call(dead)     # 2 échecs…
    b.call(lambda: "ok")                         # …un succès intercalé…
    with raises(Transient): b.call(dead)         # …puis un 3e échec : le circuit
    assert b.state == "closed"                   # reste FERMÉ. consécutifs = consécutifs.
… ou écris-le ici, en JavaScript… or write it here, in JavaScripttourne dans ton navigateur · rien n’est envoyéruns in your browser · nothing is sent
⌘/Ctrl + ↵⌘/Ctrl + ↵
Chez toi, cette semaine.In your codebase, this week.

Cherche ton candidat n°1 au disjoncteur : un appel réseau vers une dépendance qui peut rester morte plusieurs minutes, sans rien au-dessus du retry. Un point de départ : grep -rn "requests\.\|fetch(\|httpx\." src/ | grep -v timeout — chaque résultat est un appel sans timeout, donc a fortiori sans disjoncteur. Pose-toi la question du chapitre 4 : « si cette dépendance reste morte dix minutes, qu'est-ce qui arrête mes appels ? »Find your #1 breaker candidate: a network call to a dependency that can stay dead for minutes, with nothing above the retry. A starting point: grep -rn "requests\.\|fetch(\|httpx\." src/ | grep -v timeout — every hit is a call without a timeout, hence a fortiori without a breaker. Ask chapter 4's question: 'if this dependency stays dead ten minutes, what stops my calls?'