Distributed Circuit Breaker that coordinates state across multiple .NET application instances using Redis. Perfect for microservices, cloud applications, and scaled systems. When any instance detects failures and breaks the circuit, ALL instances immediately stop calling the failing service via Redis coordination. Works with Azure Redis Cache, AWS ElastiCache, Redis Cloud, and self-hosted Redis. Simple fluent API, automatic fallback, distributed locking to prevent race conditions.
$ dotnet add package CircuitBreaker.Redis.DistributedDistributed Circuit Breaker for .NET that coordinates state across multiple application instances using Redis.
When one instance detects failures and breaks the circuit, ALL instances immediately stop calling the failing service. No more cascading failures!
dotnet add package CircuitBreaker.Redis.Distributed
using CircuitBreaker.Redis.Distributed;
// Create circuit breaker
var cb = DistributedCircuitBreaker.Create("payment-api", "localhost:6379");
// Simple call with automatic fallback - NO EXCEPTIONS!
var result = await cb.CallWithFallback(
primary: () => CallPaymentGateway(amount),
fallback: () => CallBackupGateway(amount),
isSuccess: (r) => r?.Success == true
);
That's it! 3 lines of code for distributed resilience.
You have 10 instances of your payment service. When the payment gateway goes down:
Instance 1: Detects failure, waits, retries, fails again...
Instance 2: Doesn't know, keeps calling, fails...
Instance 3: Doesn't know, keeps calling, fails...
...
Instance 10: All failing, users frustrated, cascading failure!
Recovery time: 25+ minutes (each instance learns independently)
With CircuitBreaker.Redis.Distributed:
Instance 1: Detects failures → Opens circuit in Redis
Instance 2: Checks Redis → Sees circuit open → Uses fallback
Instance 3: Checks Redis → Sees circuit open → Uses fallback
...
All instances: Immediate fallback, users happy!
Recovery time: 10 seconds (coordinated via Redis)
| Feature | Description |
|---|---|
| 🌐 Distributed State | All instances share circuit state via Redis |
| 🔄 Multiple Fallbacks | Chain fallbacks that try in order |
| ✅ Custom Success Check | YOU define what "success" means |
| 🔒 Distributed Locking | Prevents race conditions on state changes |
| 📊 Sliding Window | Only counts recent failures (configurable) |
| 🔌 Works Everywhere | Azure Redis, AWS ElastiCache, Redis Cloud, self-hosted |
| 💾 Auto Fallback | Works even if Redis is down (uses local memory) |
| 📡 State Callbacks | Get notified when circuit opens/closes |
var result = await cb.CallWithFallback(
primary: () => CallPrimaryApi(),
fallback: () => CallBackupApi(),
isSuccess: (r) => r?.StatusCode == 200
);
// result is ALWAYS valid (from primary or fallback)
// No try-catch needed!
var result = await cb.CallWithFallback(
primary: () => CallAzureApi(),
isSuccess: (r) => r?.Data != null,
fallbacks: new[] {
() => CallAwsApi(), // Try this first
() => CallGcpApi(), // Then this
() => GetCachedData() // Last resort
}
);
var result = await cb.CallWithFallback(
primary: () => CallExternalService(),
fallback: () => GetCachedResponse(),
isSuccess: (r) => r != null,
onCircuitOpen: () => logger.LogWarning("Circuit opened!"),
onFallbackUsed: () => logger.LogInfo("Using fallback")
);
var cb = DistributedCircuitBreaker.Create(config => config
.WithCircuitId("payment-gateway")
.WithRedis("your-cache.redis.cache.windows.net:6380,ssl=True,password=xxx")
.FailWhen(failureRatio: 0.5, minimumCalls: 5)
.StayOpenFor(TimeSpan.FromSeconds(30))
.MeasureFailuresOver(TimeSpan.FromSeconds(10))
.OnStateChange(change =>
{
logger.LogWarning($"Circuit {change.CircuitId} → {change.NewState}");
})
);
// Program.cs
builder.Services.AddDistributedCircuitBreaker("payment", b => b
.WithRedis(connectionString)
.FailWhen(0.5, 5)
);
// PaymentService.cs
public class PaymentService
{
private readonly IDistributedCircuitBreaker _cb;
public PaymentService(
[FromKeyedServices("payment")] IDistributedCircuitBreaker cb)
{
_cb = cb;
}
public async Task<PaymentResult> ProcessPayment(decimal amount)
{
return await _cb.CallWithFallback(
primary: () => _primaryGateway.Charge(amount),
fallback: () => _backupGateway.Charge(amount),
isSuccess: (r) => r?.Approved == true
);
}
}
┌──────────────────────────────────────────────────┐
│ │
│ CLOSED ────(failures exceed threshold)────► │
│ │ OPEN │
│ │ │ │
│ ◄──(probe succeeds)── HALF-OPEN ◄──┘ │
│ │ │
│ (probe fails)────────────► │
│ │
└──────────────────────────────────────────────────┘
| State | Allows Calls? | Description |
|---|---|---|
| Closed | ✅ Yes | Normal operation |
| Open | ❌ No | Blocking calls, using fallback |
| Half-Open | ⚠️ One | Testing if service recovered |
cb:{circuitId}:state → "Closed" | "Open" | "HalfOpen"
cb:{circuitId}:metrics → { successCount, failureCount, windowStart }
cb:{circuitId}:blocked → Timestamp when circuit opened
cb:{circuitId}:lock → Distributed lock token
| Option | Default | Description |
|---|---|---|
CircuitId | Required | Unique ID - instances with same ID share state |
RedisConnection | Required | Redis connection string |
FailureRatio | 0.5 (50%) | Break when failures exceed this ratio |
MinimumCalls | 5 | Min calls before circuit can break |
BreakDuration | 30s | How long circuit stays open |
SamplingWindow | 10s | Sliding window for failure tracking |
FallbackToMemory | true | Use local memory if Redis unavailable |
// Azure Redis Cache
.WithRedis("your-cache.redis.cache.windows.net:6380,ssl=True,password=xxx")
// AWS ElastiCache
.WithRedis("your-cluster.amazonaws.com:6379")
// Redis Cloud
.WithRedis("redis-12345.cloud.redislabs.com:12345,password=xxx")
// Self-hosted / Docker
.WithRedis("localhost:6379")
Tested with Azure Redis Cache:
| Metric | Value |
|---|---|
| Throughput | 460+ requests/second |
| Average Latency | 104ms |
| P99 Latency | 213ms |
| Recovery Time | 10 seconds (vs 25 min without) |
// Primary: Stripe, Fallback: PayPal
var payment = await cb.CallWithFallback(
primary: () => _stripe.Charge(amount),
fallback: () => _paypal.Charge(amount),
isSuccess: (r) => r?.Approved == true,
onFallbackUsed: () => _metrics.IncrementPaypalUsage()
);
// Primary: Read/Write DB, Fallback: Read Replica
var user = await cb.CallWithFallback(
primary: () => _primaryDb.GetUser(id),
fallback: () => _replicaDb.GetUser(id),
isSuccess: (r) => r != null
);
// Primary: Live API, Fallback: Cached data
var data = await cb.CallWithFallback(
primary: () => _externalApi.GetData(),
fallback: () => _cache.GetData(),
isSuccess: (r) => r?.IsValid == true
);
var response = await cb.CallWithFallback(
primary: () => CallUsEastApi(),
isSuccess: (r) => r?.Success == true,
fallbacks: new[] {
() => CallUsWestApi(),
() => CallEuApi(),
() => GetCachedResponse()
}
);
public interface IDistributedCircuitBreaker
{
// Properties
string State { get; } // Current state
bool IsAllowingCalls { get; } // Can calls go through?
bool IsHealthy { get; } // Is circuit closed?
// Simple call with fallback (v2.0)
Task<T> CallWithFallback<T>(
Func<Task<T>> primary,
Func<Task<T>> fallback,
Func<T, bool> isSuccess,
Action? onCircuitOpen = null,
Action? onFallbackUsed = null);
// Multiple fallbacks (v2.0)
Task<T> CallWithFallback<T>(
Func<Task<T>> primary,
Func<T, bool> isSuccess,
Func<Task<T>>[] fallbacks,
Action? onCircuitOpen = null,
Action? onFallbackUsed = null);
// Manual control
Task Open(); // Block all calls
Task Close(); // Resume calls
}
v2.0 is fully backward compatible. Your existing code works!
// v1.x code (still works)
using Polly.Redis; // Old namespace still works
var cb = CircuitBreaker.Create("api", "redis");
// v2.0 code (recommended)
using CircuitBreaker.Redis.Distributed; // New namespace
var cb = DistributedCircuitBreaker.Create("api", "redis");
New in v2.0:
CallWithFallback - Simple API, no exceptions neededisSuccess - YOU define what success meansContributions are welcome! Please read our Contributing Guide.
BSD-3-Clause License. See LICENSE for details.
Made with ❤️ by Sanket Singh