Circuit Breaker Pattern
When a downstream service is slow or failing, callers keep trying — and accumulate threads, connections, and timeouts. The Circuit Breaker pattern detects this and stops calls to the failing service, allowing it to recover.
How It Works
Section titled “How It Works”CLOSED → Normal operation. Calls pass through. → Failure threshold reached → trips to OPEN
OPEN → All calls fail immediately (no attempt made). → After timeout → moves to HALF-OPEN
HALF-OPEN → Allows a limited number of test calls. → If they succeed → back to CLOSED → If they fail → back to OPENPolly (C# — Recommended Library)
Section titled “Polly (C# — Recommended Library)”dotnet add package Pollydotnet add package Microsoft.Extensions.Http.PollyBasic circuit breaker:
var circuitBreaker = Policy .Handle<HttpRequestException>() .CircuitBreakerAsync( exceptionsAllowedBeforeBreaking: 3, durationOfBreak: TimeSpan.FromSeconds(30), onBreak: (ex, duration) => logger.LogWarning("Circuit opened for {Duration}s: {Error}", duration.TotalSeconds, ex.Message), onReset: () => logger.LogInformation("Circuit closed — service recovered"), onHalfOpen: () => logger.LogInformation("Circuit half-open — testing service") );Advanced: Polly v8 with Resilience Pipeline:
builder.Services.AddHttpClient<IPaymentServiceClient, PaymentServiceClient>() .AddResilienceHandler("payment-pipeline", builder => { builder.AddCircuitBreaker(new CircuitBreakerStrategyOptions { FailureRatio = 0.5, // trip when 50% of calls fail MinimumThroughput = 10, // require at least 10 calls before tripping SamplingDuration = TimeSpan.FromSeconds(30), BreakDuration = TimeSpan.FromSeconds(15), });
builder.AddRetry(new RetryStrategyOptions { MaxRetryAttempts = 2, Delay = TimeSpan.FromMilliseconds(200), BackoffType = DelayBackoffType.Exponential });
builder.AddTimeout(TimeSpan.FromSeconds(5)); });Using it:
public class PaymentServiceClient : IPaymentServiceClient{ private readonly HttpClient _http;
public PaymentServiceClient(HttpClient http) => _http = http;
public async Task<PaymentResult> ChargeAsync(ChargeRequest request) { // Resilience pipeline is applied automatically via AddResilienceHandler var response = await _http.PostAsJsonAsync("/payments/charge", request); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync<PaymentResult>(); }}Fallback Behaviour
Section titled “Fallback Behaviour”When the circuit is open, return a fallback instead of an error:
var pipeline = new ResiliencePipelineBuilder<UserProfile>() .AddFallback(new FallbackStrategyOptions<UserProfile> { ShouldHandle = new PredicateBuilder<UserProfile>() .Handle<BrokenCircuitException>() .Handle<HttpRequestException>(), FallbackAction = args => { // Return cached data or a default response var cached = _cache.Get<UserProfile>(userId); return ValueTask.FromResult(Outcome.FromResult(cached ?? UserProfile.Guest())); } }) .AddCircuitBreaker(new CircuitBreakerStrategyOptions<UserProfile> { ... }) .Build();Bulkhead Pattern (Companion to Circuit Breaker)
Section titled “Bulkhead Pattern (Companion to Circuit Breaker)”Isolate thread pools per dependency so one slow service can’t exhaust all threads:
builder.AddConcurrencyLimiter(new ConcurrencyLimiterStrategyOptions{ MaxConcurrentExecutions = 10, // max 10 parallel calls to this service QueuedTasksLimit = 5 // queue up to 5 more before rejecting});Monitoring Circuit State
Section titled “Monitoring Circuit State”Expose circuit breaker state in health checks:
builder.Services.AddHealthChecks() .AddCheck("payment-service-circuit", () => { return circuitBreakerState switch { CircuitState.Closed => HealthCheckResult.Healthy("Circuit closed"), CircuitState.HalfOpen => HealthCheckResult.Degraded("Circuit half-open"), CircuitState.Open => HealthCheckResult.Unhealthy("Circuit open — service unavailable"), _ => HealthCheckResult.Healthy() }; });When to Use
Section titled “When to Use”| Scenario | Use Circuit Breaker? |
|---|---|
| Calling external HTTP services | Yes |
| Database calls | Yes (separate breaker per DB) |
| Calling internal services | Yes |
| Simple in-process method calls | No |
| Message queue consumers | Usually no — queue handles back-pressure |
Combining with Retry
Section titled “Combining with Retry”Always put the circuit breaker outside the retry:
Request → [Circuit Breaker] → [Retry] → [Timeout] → ServiceThis way, the circuit breaker counts the final failure after retries are exhausted, not each retry attempt individually.