Quand un service externe défaille, le vrai risque n’est pas sa panne, c’est qu’il entraîne tout votre système dans sa chute. Un appel qui ne répond plus, ce sont des connexions qui s’accumulent, des threads bloqués, un event loop saturé, et de proche en proche une application entière qui s’effondre. Le Circuit Breaker est le pattern qui empêche cet effet domino.
Ce troisième volet de la série sur la résilience prolonge naturellement les deux précédents : si vos retries (épisode 1) et vos timeouts (épisode 2) déclenchent en boucle sans jamais aboutir, c’est le signe que le service d’en face est probablement down. Et qu’il est temps d’arrêter de frapper à une porte fermée.
Le principe : un disjoncteur pour vos services
Vous connaissez le disjoncteur électrique : quand un appareil défaille, il coupe le circuit pour protéger l’installation. Le Circuit Breaker applique exactement cette idée au logiciel. Un appareil défectueux sur le réseau électrique, c’est l’équivalent d’un service tiers défaillant dans votre architecture distribuée : si on ne l’isole pas, il met tout le reste en danger.
Concrètement, le Circuit Breaker s’intercale comme un proxy entre votre système et le service externe. Il surveille les appels qui le traversent et décide, selon leur résultat, de les laisser passer ou de les couper immédiatement. C’est ni plus ni moins le disjoncteur d’une architecture résiliente : quand un service chauffe, on coupe avant que tout parte en fumée.
Une machine à états à trois positions
Le cœur du pattern est une machine à états. À tout instant, le breaker se trouve dans l’un de ces trois états :
| État | Comportement |
|---|---|
| CLOSED | Tout fonctionne normalement : les requêtes passent vers le service. |
| OPEN | Le service est considéré down : les requêtes sont bloquées immédiatement, sans même tenter l’appel. |
| HALF-OPEN | État de transition : on laisse passer quelques appels, de façon contrôlée et limitée, pour tester si le service est de retour. |
Ce mécanisme apporte trois bénéfices directs :
- Il préserve la stabilité du reste de l’application en la protégeant contre l’épuisement des ressources (connexions, threads, mémoire) mobilisées par le service défaillant.
- Il allège la pression continue sur un service déjà en difficulté, lui laissant une chance de se rétablir au lieu de l’achever sous les requêtes.
- Il amorce une reprise contrôlée et progressive des interactions, plutôt qu’un retour brutal au plein régime.
Des transitions automatiques
Tout l’intérêt du pattern est que ces changements d’état sont gérés pour vous, en fonction de seuils que vous définissez :
- CLOSED → OPEN : après N erreurs consécutives, ou N requêtes dont la latence dépasse un seuil, le circuit s’ouvre et coupe le trafic.
- OPEN → HALF-OPEN : passé un certain délai, le breaker amorce une transition pour sonder avec précaution l’état du service.
- HALF-OPEN → CLOSED : si les appels de sondage réussissent, on referme le circuit et tout reprend normalement. Dans le cas contraire, retour en OPEN, et on attend de nouveau.
En pratique avec Node.js
Inutile de réimplémenter cette machine à états vous-même : le pattern est très largement supporté par des bibliothèques matures. L’exemple ci-dessous utilise cockatiel pour protéger un appel base de données. On configure un breaker qui s’ouvre après 5 échecs consécutifs et reste ouvert 10 secondes avant de tenter une reprise.
/**
* Le pattern Circuit Breaker permet de protéger notre application
* contre l'effet domino suite à une panne d'un service externe.
*/
import {
ConsecutiveBreaker,
ExponentialBackoff,
retry,
handleAll,
circuitBreaker,
wrap,
} from "cockatiel";
/**
* Ici on définit un Circuit Breaker qui arrête d'appeler la fonction
* exécutée pendant 10 secondes si elle échoue 5 fois consécutivement.
* Il existe d'autres types de Breakers, qui permettent d'affiner
* et d'ajuster la stratégie de résilience.
*/
const breaker = circuitBreaker(handleAll, {
halfOpenAfter: 10 * 1000,
breaker: new ConsecutiveBreaker(5),
});
const dbCallWithBreakerAndTimeout = () =>
breaker.execute(() =>
knex.select().from("books").timeout(10_000, { cancel: true })
);
dbCallWithBreakerAndTimeout()
.then(() => {
// [...]
})
.catch(() => {
// [...]
});
/**
* On peut même combiner le Circuit Breaker avec un pattern Retry + Timeout
* de manière à rendre l'appel initial plus résilient avant de déclencher
* le comportement du Circuit Breaker.
*/
const retryPolicy = retry(handleAll, {
maxAttempts: 3,
backoff: new ExponentialBackoff(),
});
const breakerWithRetry = wrap(retryPolicy, breaker);
const fullyResilientDbCall = () =>
breakerWithRetry.execute(() =>
knex.select().from("books").timeout(10_000, { cancel: true })
);
L’intérêt du dernier bloc mérite d’être souligné : le Circuit Breaker ne s’oppose pas aux patterns vus précédemment, il les complète. En enveloppant le breaker avec une politique de retry et un timeout (wrap), on rend chaque appel plus résilient avant d’envisager d’ouvrir le circuit. Le breaker ne s’ouvre alors que lorsque, malgré les tentatives, le service reste durablement indisponible : exactement le signal qu’on veut détecter.
Un pattern disponible partout
Bonne nouvelle : ce pattern est tellement répandu qu’il existe une implémentation éprouvée dans pratiquement tous les écosystèmes, ce qui le rend facile à adopter :
- Node.js : opossum, cockatiel
- .NET : Polly
- Java / Kotlin : resilience4j
- Go : gobreaker
Conclusion
Le Circuit Breaker ne répare pas un service en panne, ce n’est pas son rôle. Il fait quelque chose de plus précieux : il empêche cette panne de devenir la vôtre. En isolant un dépendant défaillant, il protège vos ressources, soulage le service en difficulté et orchestre une reprise progressive une fois la tempête passée.
Combiné aux retries et aux timeouts des épisodes précédents, il forme le socle d’une stratégie de résilience cohérente. Prenez les devants : mieux vaut couper un circuit volontairement que de regarder tout le système partir en fumée.