Skip to content

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.

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 OPEN
Terminal window
dotnet add package Polly
dotnet add package Microsoft.Extensions.Http.Polly

Basic 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>();
}
}

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
});

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()
};
});
ScenarioUse Circuit Breaker?
Calling external HTTP servicesYes
Database callsYes (separate breaker per DB)
Calling internal servicesYes
Simple in-process method callsNo
Message queue consumersUsually no — queue handles back-pressure

Always put the circuit breaker outside the retry:

Request → [Circuit Breaker] → [Retry] → [Timeout] → Service

This way, the circuit breaker counts the final failure after retries are exhausted, not each retry attempt individually.