Aplikacja w formie samowystarczalnego monolitu, która nie komunikuje się z innymi systemami ani zewnętrznymi usługami, jest dziś raczej rzadko spotykana. Każdy kolejny element, z którym możemy się komunikować, to potencjalne źródło problemów, które ostatecznie mogą wpływać na dostępność i poprawność działania naszej aplikacji.
Circuit breaker to wzorzec służący do obsługi takich sytuacji. Podobnie jak bezpiecznik w instalacji elektrycznej, wzorzec odcina komunikację z niedostępnym komponentem, zanim problem zacznie wpływać na resztę systemu.
W artykule omówię charakterystykę wzorca i kiedy warto go stosować. Pokażę też przykład implementacji i praktyczne przypadki, gdzie wzorzec mógłby mieć zastosowanie.
Charakterystyka wzorca
Nazwa wzorca dobrze oddaje jego cel oraz sposób działania w systemie. W przypadku obwodu elektrycznego prąd płynie przez bezpiecznik. Z kolei w przypadku omawianego wzorca zapytanie przechodzi przez breaker i jest przekazywane dalej. Gdy liczba kolejnych niepowodzeń przekroczy wcześniej ustalony próg, mechanizm zadziała.
Przez określony czas wszystkie kolejne próby wywołania usługi zdalnej będą natychmiast kończyć się niepowodzeniem. Po upływie czasu breaker przepuszcza ograniczoną liczbę zapytań testowych. Jeśli zakończą się powodzeniem, breaker wraca do normalnego działania. W przeciwnym razie mechanizm nadal blokuje dostęp do usługi, powtarzając po wskazanym czasie test dostępności.
Takie zachowanie pozwala chronić system przed kaskadowym propagowaniem błędów. Czasami problem może doprowadzić do wyczerpania zasobów aplikacji, np. połączeń czy workerów, co może wpływać także na inne jego części. Zamiast ponawiać zapytania, szybko odrzucamy zapytania, które prawdopodobnie zakończyłyby się błędem.
Pozwala to też np. na szybsze odzyskanie sprawności systemu przez wykorzystanie fallbacków do innych usług czy odciążenie awaryjnego komponentu – czasami awaria może wynikać po prostu ze zbyt dużego ruchu.
Logikę circuit breakera można opisać trzema stanami:
closed– jest to domyślny stan, gdy monitorowana usługa jest dostępna i komunikacja przebiega prawidłowo.open– gdy liczba lub odsetek nieudanych zapytań przekroczy skonfigurowany próg w danym oknie czasowym, breaker przechodzi w stanopen. W tym momencie następuje odcięcie komunikacji z niedziałającą usługą i ewentualne wykonanie fallbacku.half-open– po upłynięciu określonego czasu od przejścia w stanopen, breaker przechodzi w stanhalf-open. W tym stanie breaker przepuszcza ograniczoną liczbę żądań testowych, aby sprawdzić, czy monitorowana usługa odzyskała stabilność. Jeśli tak, to stan breakera zmienia się naclosed; jeśli nie, to wraca do wartościopen.
Przejścia między poszczególnymi stanami i warunki zmiany stanów przedstawia poniższy diagram.

Implementując circuit breakera dla konkretnego przypadku, warto się zastanowić:
- jaki threshold niepowodzeń w jakim czasie ma spowodować zmianę stanu na
closed; - jak długo chcemy, by breaker był w stanie
open; - ile zapytań testowych chcemy wykonać w stanie
half-open;
Jeśli od samego początku działania danej integracji chcemy wykorzystać circuit breaker, to rzetelne wskazanie tych parametrów może stanowić problem. Jeśli nie mamy pewności co do zasadności wykorzystania breakera, to rozważyłbym wstrzymanie się z przedwczesną implementacją. Myślę, że warto to zrobić przynajmniej do momentu zebrania rzetelnych danych pozwalających na oszacowanie wskazanych parametrów. Poza problemami z precyzyjnym wyznaczeniem parametrów może również zwyczajnie okazać się, że sam breaker jest zbędny. Czasami stabilność zewnętrznej usługi będzie wystarczająca lub potencjalne szkody na tyle niewielkie, że dodatkowy mechanizm zabezpieczający przyniesie marginalne korzyści.
Warto również dla każdego przypadku rozważyć, co powinno wydarzyć się, gdy breaker jest w stanie open. Czasami możemy po prostu rzucić błędem. Jednak czasami możemy chcieć np. wykonać fallback w postaci wysłania zapytania w inne miejsce lub zwrócenia alternatywnej odpowiedzi do klienta.
Przykłady wykorzystania w praktyce
Najbardziej klasycznym przypadkiem wykorzystania circuit breakera jest sytuacja, gdy zewnętrzna usługa nie działa. Przykładowo, w systemie e-commerce może nie działać bramka płatności, np. w wyniku awarii czy prac konserwacyjnych. W takiej sytuacji możemy przekierować użytkownika do innej formy płatności.
Czasami możemy potrzebować danych z niedziałającej usługi. W takiej sytuacji breaker może odciąć ruch do tej usługi i możemy zrobić fallback na cache lub zwrócić niekompletne dane. Czasami dana funkcja nie jest kluczowa dla działania systemu i możemy ją pominąć. Na przykład, jeśli w naszym sklepie wykorzystujemy zewnętrzny system oceny produktów przez klientów i jest on niedostępny, to możemy po prostu pominąć opinie przy zwracaniu informacji o produkcie.
Poza samą niedostępnością zewnętrznej usługi jest szereg innych okoliczności, w których circuit breaker może mieć zastosowanie. Jeśli zewnętrzna usługa działa, ale robi to zbyt wolno, to można rozważyć przekroczenie określonego progu czasu odpowiedzi jako błąd. Kolejnym przypadkiem jest wpadnięcie w rate limit. Oba przypadki są dość powszechne, np. w pracy z LLM-ami. Sytuacje nadmiernego obciążenia providerów czy konkretnych modeli zdarzają się często. Możemy wtedy przekierować nasze zapytanie do innego modelu czy dostawcy.
Przykładowa implementacja
W zrozumieniu działania breakera najbardziej pomoże uproszczony przykład implementacji. W tym przypadku wykorzystałem TypeScripta oraz Redisa (klient ioredis) do przechowywania stanu breakera. Wykorzystanie zewnętrznego storage powoduje, spójne zachowanie dla wszystkich instancji aplikacji. Dla uproszczenia przykładu kod wymaga jedynie jednego sukcesu, by przejść ze stanu half-open do closed.
enum CircuitState {
CLOSED = 'closed',
OPEN = 'open',
HALF_OPEN = 'half-open',
}
type CircuitBreakerOptions = {
failureThreshold: number;
recoveryTimeoutSeconds: number;
};
export class CircuitBreaker {
constructor(
private readonly redis: RedisClient,
private readonly name: string,
private readonly options: CircuitBreakerOptions,
) {}
async execute<T>( operation: () => Promise<T> ): Promise<T> {
const state = await this.getState();
if ( state === CircuitState.OPEN ) {
const isOpen = await this.redis.exists( this.openKey() );
if ( isOpen ) {
throw new Error( 'Circuit breaker is OPEN' );
}
await this.setState( CircuitState.HALF_OPEN );
}
try {
const result = await operation();
await this.onSuccess( state );
return result;
} catch ( error ) {
await this.onFailure( state );
throw error;
}
}
private async onSuccess( state: CircuitState ): Promise<void> {
if ( state === CircuitState.HALF_OPEN ) {
await Promise.all( [
this.setState( CircuitState.CLOSED ),
this.redis.set( this.failuresKey(), 0 ),
this.redis.del( this.openKey() )
] );
}
}
private async onFailure( state: CircuitState ): Promise<void> {
const failures = await this.redis.incr( this.failuresKey() );
if (failures === 1) {
await this.redis.expire( this.failuresKey(), 60 );
}
if ( state === CircuitState.HALF_OPEN || failures >= this.options.failureThreshold ) {
await Promise.all([
this.setState( CircuitState.OPEN ),
this.redis.setex(
this.openKey(),
this.options.recoveryTimeoutSeconds,
'1'
)
] );
}
}
private async getState(): Promise<CircuitState> {
const state = await this.redis.get( this.stateKey() );
if ( !state ) {
return CircuitState.CLOSED;
};
if ( !Object.values( CircuitState ).includes( state as CircuitState ) ) {
throw new Error( `Invalid circuit breaker state: ${ state }` );
}
return state as CircuitState;
}
private async setState( state: CircuitState ): Promise<void> {
await this.redis.set( this.stateKey(), state );
}
private stateKey(): string {
return `cb:${this.name}:state`;
}
private failuresKey(): string {
return `cb:${this.name}:failures`;
}
private openKey(): string {
return `cb:${this.name}:open`;
}
}
Mając gotowy komponent, wystarczy go wdrożyć do integracji z zewnętrznym systemem.
const paymentsCircuitBreaker = new CircuitBreaker(
redisClient,
'payments-api',
{
failureThreshold: 5,
recoveryTimeoutMs: 10_000
},
);
async function executePayment() {
return paymentsCircuitBreaker.execute(
async () => {
const response = await fetch( '...' );
if ( !response.ok ) {
throw new Error( 'Payments API failed' );
}
return response.json();
}
);
}
Podany przykład jest bardzo uproszczony i służy do zrozumienia istoty działania wzorca. Jednak nie nadaje się do wdrożenia produkcyjnego. W pokazanym kodzie jest kilka aspektów wartych poprawy:
- Równoczesne wykonanie wielu zapytań po przejściu circuit breakera w stan
half-openmoże spowodować wysłanie większej liczby zapytań testowych niż zakładamy. - Możliwość wywołania fallbacka, np. wyboru alternatywnego API do płatności. Czasami można również zakolejkować retry akcji i spróbować wykonać ją ponownie za jakiś czas.
- Detekcja nie tylko jawnych faili, ale też wolnych zapytań. W podanym przykładzie z płatnością długie procesowanie płatności może spowodować, że kupujący jednak zrezygnuje. Wiedząc, że główne API do płatności wolno odpowiada, moglibyśmy przełączyć się na fallback.
- Wdrożenie sliding window lub inne bardziej zaawansowane liczenie błędów w czasie zamiast prostego podejścia opartego na tumbling window.
- Możliwość podpięcia logów, metryk i alertów informujących o problemach z usługą.
- Co w przypadku, gdy Redis nie działa? W podanej implementacji dostępność Redisa jest wąskim gardłem. Czy w takim przypadku logika powinna operować na jakimś cache in-memory czy po prostu przepuszczać zapytania (przełączyć się w stan
closed)?
Warto również rozważyć skorzystanie z gotowych bibliotek, takich jak Opossum dla Node.js, Polly w C# czy Resilience4j dla Javy. Czasami nie ma sensu inwestować we własną implementację. Zamiast tego można wykorzystać gotowca i skupić się na celach biznesowych.
Podsumowanie
Warto zapoznać się z koncepcją circuit breakera w praktyce. Obecnie coraz więcej odpowiedzialności delegujemy do zewnętrznych usług. Również wykorzystanie LLM-ów, które nie słyną z niezawodności, rośnie w ogromnym tempie. Odpowiednia obsługa problemów z ich dostępnością jest kluczowa w dowożeniu dobrze działającego softu.
Daj znać w komentarzu, jeśli dzięki temu artykułowi udało Ci się dowiedzieć czegoś ciekawego. Jeśli masz jakieś ciekawe przemyślenia lub doświadczenia z wykorzystaniem circuit breakera, to również zachęcam do podzielenia się nimi pod artykułem.
Źródła i materiały dodatkowe
- Martin Fowler – Circuit Breaker
- Microsoft Azure Architecture Center – Circuit Breaker pattern
- AWS Perspective Guidance – Circuit breaker pattern
- Marc’s Blog – Will circuit breakers solve my problems?
- Marc’s Blog – Fixing retries with token buckets and circuit breakers
- Building Microservices. 2nd Edition – Sam Newman (str. 401)
- Patryk Bernacki – Circuit Breaker not only for HTTP calls! (based on resilience4j)
- Nick Chapas – The Circuit Breaker Pattern | Resilient Microservices


Kolejna książka o Gicie — naucz się korzystać z Gita jak profesjonalista
"Kolejna książka o Gicie" to kompleksowy e-book, który pozwoli Ci poznać Gita od A do Z, a także liczne narzędzia dedykowane pracy z Gitem!
Dlaczego warto?
Przygotuj się lepiej do rozmowy o pracę!
Odbierz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer i realnie zwiększ swoje szanse na rozmowie rekrutacyjnej! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.
Dlaczego warto?
E-booka odbierzesz korzystając z formularza poniżej 👇