Le pattern « Circuit Breaker » : un bouclier pour nos services
Les micro-services c'est bien, quand il y en a un qui répond plus ça peut vite être la cata... Sauf si on prévoit les choses !
Article écrit par Vincent Du !
Nous l’avons tous vécu : un service externe sur lequel nous nous appuyons commence à répondre lentement… puis finit par ne plus répondre du tout. Pendant ce temps, nos serveurs continuent d’envoyer des requêtes comme si de rien n’était. Résultat : files d’attente qui grossissent, threads d’exécution saturés, et une application qui devient peu à peu inutilisable. C’est exactement ce qu’on appelle une défaillance en cascade.
C’est là qu’intervient le circuit breaker. Inspiré du disjoncteur électrique de nos maisons, il agit comme un garde-fou : quand un service commence à tomber, il coupe rapidement les appels pour éviter de tout faire sauter. Au lieu de laisser les requêtes s’empiler, nous échouons tout de suite avec une réponse claire (par exemple une erreur 503 service indisponible). L’idée : mieux vaut couper le courant à temps que tout faire sauter.
Comment fonctionne un circuit breaker ?
Le circuit breaker a trois états principaux :
- Fermé : Tout est normal, les appels passent directement au service.
- Ouvert : Après un certain nombre d’échecs consécutifs, le circuit breaker « saute » et coupe instantanément tous les appels suivants.
- Semi-ouvert : Après un délai de repos, le circuit breaker autorise quelques appels tests pour voir si le service est revenu. S’ils réussissent, nous refermons le circuit. Si ces essais échouent, le circuit reste ouvert.

On peut l’imaginer comme un disjoncteur électrique : tant que le courant passe, tout va bien. Mais en cas de surcharge, il saute, protège votre installation, puis se réenclenche automatiquement quand la situation se stabilise.
Pourquoi ce pattern améliore l’expérience utilisateur ?
Le circuit breaker n’a pas pour seul objectif la résilience technique : il protège aussi l’expérience utilisateur. En échouant rapidement ou en servant une alternative dégradée, nous évitons les blocages et latences longues qui frustrent l’utilisateur. Cette dégradation contrôlée est appelée un fallback et peut prendre plusieurs formes :
- Retourner une erreur structurée pour informer l’utilisateur.
- Servir une valeur issue d’un cache avec une politique de péremption staleness en se basant sur une durée de vie TTL (time to live) raisonnable.
- Basculer vers une implémentation de secours (ex. : API secondaire, copie locale).
- Mettre en file d’attente l’opération pour un traitement asynchrone.
Un fallback bien pensé améliore réellement l’expérience utilisateur à condition qu’il soit pertinent. Servir une donnée en cache peut convenir pour actualiser une page météo, mais est inadapté pour finaliser un paiement où la donnée doit être à jour.
Par exemple, sur l’un de mes projets, un service externe a connu une période de lenteur. Plusieurs utilisateurs étaient mécontents, mais en servant temporairement des données en cache, nous avons pu maintenir une expérience fluide et réduire significativement les retours négatifs.
Comprendre pourquoi le circuit breaker améliore l’expérience utilisateur est une première étape. La suivante consiste à voir comment l’intégrer efficacement dans un projet réel.
Exemple d’intégration : un Connector enveloppé par un circuit breaker
Le circuit breaker n’est pas qu’un try-catch autour d’un appel http. L’idée est d’intégrer ce mécanisme de manière structurée dans un projet déjà existant, pour qu’il devienne un composant réutilisable et transparent. Dans une architecture hexagonale, par exemple, le circuit breaker s’implante naturellement comme un adaptateur autour d’un port externe.
Indépendamment de l’architecture, nous pouvons définir une abstraction appelée Connector représentant tout élément capable de fournir des données à notre projet. Peu importe d’où viennent les données (requête API, cache, base de données) nous utilisons toujours les méthodes du Connector.
Ensuite, chaque implémentation de notre abstraction se charge d’un mode d’accès particulier :
- HttpConnector pour appeler des API.
- CacheConnector pour récupérer des données locales mises en cache.
- DbConnector pour interroger une base de données interne.
Pour ajouter une couche de résilience sans casser ce qui existe de façon transversale, nous pouvons introduire un nouveau type : le CircuitBreakerConnector.
- Le CircuitBreakerConnector prend en paramètre un Connector existant (ex : HttpConnector).
- Il implémente la même interface que les autres implémentations (call, get, etc.).
- À chaque appel, il n’exécute pas directement la méthode du Connector déjà existant : il la fait passer par un circuit breaker.
- Si le circuit breaker est fermé, l’appel est transmis normalement.
Si le circuit breaker est ouvert, nous appliquons un fallback (par ex. : une erreur 503 ou récupération dans un cache).

Grâce à cette approche :
- La transparence reste totale : le reste du code n’a pas besoin de savoir qu’un circuit breaker est utilisé. Il continue à appeler la méthode de l’interface comme d’habitude.
- La résilience est interchangeable : nous pouvons envelopper n’importe quelle implémentation (HTTP, DB, cache distant) dans un circuit breaker.
- Nous pouvons mixer plusieurs stratégies : par exemple un HttpConnector enveloppé par un CircuitBreakerConnector, et en fallback un CacheConnector.
Exemple en Node.js avec Opossum
Voici le diagramme de l’application que nous allons implémenter, une simple application qui fait une requête vers une API externe :

Définissons d’abord l’interface Connector et son implémentation HttpConnector avec une simple méthode countUsersWithName() qui se contente de faire un appel HTTP classique à une API externe.
// Interface générique
interface Connector {
countUsersWithName(name: string): Promise<number>;
}
// Implémentation HTTP du Connector
class HttpConnector implements Connector {
async countUsersWithName(name: string): Promise<number> {
// Appel à une API externe qui retourne le nombre d'utilisateurs avec ce nom
const response = await http.get(`https://api.example.com/users/count?name=${name}`);
return response.data.count;
}
}Jusqu’ici, rien de très complexe. Mais dès que la dépendance externe devient instable, ce genre d’appel peut bloquer tout le système. Rajoutons y un circuit breaker.
Il est tout à fait possible de l’implémenter soi-même, en gérant les compteurs d’échecs, les délais avant réouverture, mais cela devient vite fastidieux à maintenir dès que les cas d’erreur se multiplient ou que plusieurs dépendances doivent être surveillées.
En Node.js, plusieurs bibliothèques comme Opossum et Cockatiel permettent d’implémenter un circuit breaker sans repartir de zéro.
Opossum encapsule n’importe quelle fonction asynchrone (comme un appel HTTP) et gère automatiquement les états du circuit. Par exemple, avec Opossum, en cas d’erreur, Opossum se met en veille et interrompt l’exécution. Elle permet également de fournir une fonction de secours fallback qui sera exécutée en cas d’erreur.
Plusieurs paramètres nous seront utiles pour savoir quand devoir ouvrir ou non le circuit breaker.
interface CircuitBreakerOptions {
timeout: number; // Délai avant d'être considéré comme échec (ms)
errorThresholdPercentage: number; // % d'échecs avant d'ouvrir le circuit
resetTimeout: number; // Délai avant de retenter après ouverture (ms)
volumeThreshold: number; // Nb min de requêtes avant de pouvoir ouvrir le circuit breaker
}En utilisant Opossum, nous allons créer un CircuitBreakerConnector capable de gérer toutes les implémentations de l’interface Connector :
import CircuitBreaker from 'opossum';
import { ServiceUnavailableError } from '@core/http/errors';
class CircuitBreakerConnector implements Connector {
// Circuit breaker d'une méthode prenant un paramètre string et retournant un number
private circuitBreaker: CircuitBreaker<[string], number>;
constructor(
private readonly defaultConnector: Connector,
breakerOptions: CircuitBreakerOptions
) {
// Nous enveloppons la méthode countUsersWithName avec le circuit breaker
this.circuitBreaker = new CircuitBreaker<[string], number>(
(name: string) => this.defaultConnector.countUsersWithName(name),
breakerOptions
);
// Fallback en cas d'indisponibilité de l'API
this.circuitBreaker.fallback((name: string, error: Error) => {
if (this.circuitBreaker.opened) {
console.warn(`[Fallback] Circuit breaker OUVERT lors de l'appel pour "${name}"`);
} else {
console.warn(`[Fallback] Circuit breaker FERME lors de l'appel pour "${name}"`);
}
// Log de l'erreur reçue de l'API
console.error(error);
// une erreur de type HTTP 503 est lancée
throw new ServiceUnavailableError('Service temporairement indisponible');
});
// Logs pour la surveillance
this.circuitBreaker.on('open', () => console.warn('Circuit ouvert'));
this.circuitBreaker.on('halfOpen', () => console.info('Tentative de réouverture'));
this.circuitBreaker.on('close', () => console.info('Circuit fermé'));
}
async countUsersWithName(name: string): Promise<number> {
// Utilisation du circuit breaker pour sécuriser l'appel
return this.circuitBreaker.fire(name);
}
}Enfin, nous passons par le CircuitBreakerConnector pour appeler la méthode countUsersWithName() de notre HttpConnector :
const breakerOptions = {
timeout: 3000, // Délai avant d'être considéré comme échec (ms)
errorThresholdPercentage: 80, // % d'échecs avant d'ouvrir le circuit
resetTimeout: 10000, // Délai avant de retenter après ouverture (ms)
volumeThreshold: 5, // Nb min de requêtes avant de pouvoir ouvrir le circuit breaker
};
const httpConnector = new HttpConnector();
const circuitBreakerConnector = new CircuitBreakerConnector(httpConnector, breakerOptions);
async function run() {
try {
const count = await circuitBreakerConnector.countUsersWithName('alice');
console.log('Nombre d\'utilisateurs nommés "alice" :', count);
} catch (err) {
console.error('Erreur :', err);
}
}
run();Grâce à cette implémentation, nous obtenons un mécanisme plus fiable, simple à configurer et directement intégrable dans n’importe quelle autre implémentation en gardant la logique de résilience centralisée.
Mais au-delà de l’aspect technique, il est essentiel de savoir dans quels cas un circuit breaker apporte une réelle valeur ajoutée.
Quand le circuit breaker est utile… et quand il ne l’est pas
Le circuit breaker brille dès qu’une dépendance externe devient lente ou instable.
- Sur un paiement : Il évite d’immobiliser l’utilisateur pendant de longues secondes en échouant rapidement et en affichant clairement “Service temporairement indisponible”.
- Pour une API de météo : Servir une ancienne donnée encore acceptable maintient une expérience utilisateur fluide le temps que le service se rétablisse, sans enjeu de temps réel strict.
- En cas de base de données saturée : Couper les appels pendant quelques secondes laisse la base de données souffler et lui permet de se rétablir sans être surchargée de requêtes supplémentaires.
Ces exemples montrent l’intérêt du circuit breaker dans de nombreux cas, mais attention : il n’est pas adapté à toutes les situations.
Il est particulièrement pertinent dans des architectures distribuées où l’on souhaite protéger une dépendance malade (services externes ou des ressources partagées) et lorsque l’on peut tolérer une dégradation (données légèrement périmées, réponse de type « temporairement indisponible »).
En revanche, il est insuffisant comme unique mécanisme pour les opérations en temps réel ou bien d’écritures critiques qui doivent absolument réussir synchroniquement (par exemple, création d’une commande, ou validation d’un transfert bancaire). Dans ces situations, seulement ouvrir le circuit risquerait des pertes de données ou des incohérences métier.
Pour renforcer la robustesse dans ces cas, il faut combiner le circuit breaker à d’autres mécanismes complémentaires comme la file d’attente (message queue), l’idempotence, le journal de transactions locales (transaction outbox) et les re tentatives (retries).
L’enjeu est donc de l’utiliser dans une stratégie de résilience plus large, en calibrant soigneusement timeouts, seuils et fallbacks selon chaque besoin, tout en tenant compte de son coût de mise en place.
Mais quel est ce coût ?
L’ajout du circuit breaker est une évolution qui a des impacts à plusieurs niveaux dans le projet.
D’un point de vue développement, il faut non seulement créer l’implémentation du circuit breaker mais également définir sa configuration (seuils, délais, fallbacks).
Les tests unitaires et d’intégration doivent désormais couvrir les transitions d’état du circuit et les comportements en cas de panne. Avec Opossum, nous pouvons simuler des échecs pour vérifier le passage en ouvert, puis le retour en semi-ouvert et la fermeture après quelques appels réussis. Il faut également tester le fallback pour s’assurer que la valeur appropriée est renvoyée lorsque le service est indisponible.
Sur le plan architectural, il peut être nécessaire de déplacer la logique de fallback ou d’ajouter des composants transverses de surveillance monitoring. Ces changements impactent la structure et demandent un temps de réflexion non négligeable.
Côté opérationnel, la mise en place de métriques (Prometheus, DataDog), de tableaux de bord et d’alertes devient indispensable pour comprendre quand un circuit s’ouvre ou reste bloqué trop longtemps. Si la stratégie de fallback repose sur un cache ou une file d’attente, il faut prévoir l’hébergement, la configuration et la sauvegarde de ces nouveaux composants.
Enfin, la maintenance, les seuils d’ouverture, de timeout et de reset doivent être ajustés dans le temps selon les comportements réels en production. Une mauvaise configuration peut dégrader l’expérience utilisateur ou masquer des incidents.
Ces contraintes techniques et opérationnelles expliquent pourquoi le circuit breaker doit être introduit de manière réfléchie dans un projet. Pour en tirer le meilleur parti, certaines pratiques se révèlent essentielles.
Bonnes pratiques et limites
- Multi-instance : Ne pas regrouper plusieurs ressources indépendantes sous un même circuit breaker. Placez le circuit breaker au niveau des implémentations un par dépendance en évitant une configuration globale qui risque de bloquer l’ensemble de l’application.
- Fallback : Rendez la dégradation explicite pour l’utilisateur en affichant un message d’indisponibilité, par exemple en indiquant la fraîcheur des données (“mis à jour il y a X min”).
- Types d’erreurs : Un circuit breaker doit compter seulement les échecs pertinents. Nous pouvons ainsi filtrer les erreurs client (4xx) et cibler surtout les pannes techniques (timeouts, erreurs 5xx, exceptions réseau) pour déclencher l’ouverture.
- Paramétrage dynamique : Les paramètres (pourcentage d’erreurs, délais, etc.) doivent rester modifiables via des configurations externes (fichiers de config, variables d’environnement, etc.) pour permettre de réagir rapidement sans redéployer le code.
- Observabilité : Surveiller ses états (ouvert, fermé, semi-ouvert), ses taux d’échec, ses temps de réponse et la fréquence de ses réouvertures pour comprendre la santé du projet. Sans monitoring, il devient compliqué de savoir si le circuit fonctionne réellement ou masque un problème plus profond.
- Documentation et Runbooks : Expliquez clairement le comportement du circuit, les procédures à suivre quand il reste ouvert.
Conclusion
Le pattern Circuit Breaker est un outil puissant. Il protège les services sains d’une dépendance malade, améliore la stabilité globale et, surtout, maintient une expérience utilisateur prévisible en cas de crise. Facile à intégrer avec des librairies comme Opossum, il s’adapte dans tous types de contextes métiers. Toutefois, il n’est pas un remède miracle : il demande une observation continue, des fallbacks intelligents, et une intégration réfléchie dans une stratégie de résilience plus large.
En somme, comme pour l’électricité : mieux vaut un petit disjoncteur qui saute… qu’un incendie qui ravage tout le système.