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

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.