CircuitBreaker.Redis.Distributed 2.0.0

dotnet add package CircuitBreaker.Redis.Distributed --version 2.0.0
                    
NuGet\Install-Package CircuitBreaker.Redis.Distributed -Version 2.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CircuitBreaker.Redis.Distributed" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CircuitBreaker.Redis.Distributed" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="CircuitBreaker.Redis.Distributed" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CircuitBreaker.Redis.Distributed --version 2.0.0
                    
#r "nuget: CircuitBreaker.Redis.Distributed, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CircuitBreaker.Redis.Distributed@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CircuitBreaker.Redis.Distributed&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=CircuitBreaker.Redis.Distributed&version=2.0.0
                    
Install as a Cake Tool

CircuitBreaker.Redis.Distributed

NuGet NuGet Downloads License

Distributed 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!

🚀 Quick Start

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.


🎯 Why This Package?

The Problem

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)

The Solution

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)


✨ Features

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

📖 Usage Examples

Simple Call with Fallback

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!

Multiple Fallbacks

var result = await cb.CallWithFallback(
    primary: () => CallAzureApi(),
    isSuccess: (r) => r?.Data != null,
    fallbacks: new[] {
        () => CallAwsApi(),        // Try this first
        () => CallGcpApi(),        // Then this
        () => GetCachedData()      // Last resort
    }
);

With Logging

var result = await cb.CallWithFallback(
    primary: () => CallExternalService(),
    fallback: () => GetCachedResponse(),
    isSuccess: (r) => r != null,
    onCircuitOpen: () => logger.LogWarning("Circuit opened!"),
    onFallbackUsed: () => logger.LogInfo("Using fallback")
);

Full Configuration

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

ASP.NET Core Dependency Injection

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

🔧 How It Works

State Machine

┌──────────────────────────────────────────────────┐
│                                                  │
│    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

Redis Keys Structure

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

⚙️ Configuration Options

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

🌐 Supported Redis Providers

// 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")

📊 Performance

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)

🔌 Real-World Scenarios

Scenario 1: Payment Gateway Failover

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

Scenario 2: Database Read Replica

// Primary: Read/Write DB, Fallback: Read Replica
var user = await cb.CallWithFallback(
    primary: () => _primaryDb.GetUser(id),
    fallback: () => _replicaDb.GetUser(id),
    isSuccess: (r) => r != null
);

Scenario 3: External API with Cache

// Primary: Live API, Fallback: Cached data
var data = await cb.CallWithFallback(
    primary: () => _externalApi.GetData(),
    fallback: () => _cache.GetData(),
    isSuccess: (r) => r?.IsValid == true
);

Scenario 4: Multi-Region Failover

var response = await cb.CallWithFallback(
    primary: () => CallUsEastApi(),
    isSuccess: (r) => r?.Success == true,
    fallbacks: new[] {
        () => CallUsWestApi(),
        () => CallEuApi(),
        () => GetCachedResponse()
    }
);

📋 API Reference

IDistributedCircuitBreaker

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
}

🔄 Migration from v1.x

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 needed
  • isSuccess - YOU define what success means
  • Multiple fallbacks
  • Better namespace

📚 Learn More


🤝 Contributing

Contributions are welcome! Please read our Contributing Guide.


📄 License

BSD-3-Clause License. See LICENSE for details.


🙏 Acknowledgments


Made with ❤️ by Sanket Singh

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0 97 1/20/2026
1.0.0 95 1/17/2026

🚀 v2.0.0 - Major API Simplification!
     
     🆕 New Features:
     • CallWithFallback - Simple API with NO exceptions needed!
     • isSuccess parameter - YOU define what success means
     • Multiple fallbacks - Chain fallbacks that try in order
     • IsHealthy property - Quick circuit health check
     • Better namespace - using CircuitBreaker.Redis.Distributed
     
     ✨ All v1.0 Features Preserved:
     • Distributed state via Redis
     • Sliding window failure tracking
     • Distributed locking (Redis SETNX)
     • State machine (Closed/Open/HalfOpen/Isolated)
     • Automatic fallback to in-memory
     • Works with Azure, AWS, Redis Cloud, self-hosted
     
     🔄 Backward Compatible:
     • Old Execute() API still works
     • Old CircuitBreaker class still works
     • Migrate at your own pace