When building modern .NET microservices, out-of-process HTTP communication is one of the most common failure points. Network glitches, service deployments, and high latency can easily cascade across your system, causing thread starvation and widespread outages.

Combining Refit (a strongly-typed REST client library) with Polly v8 (the standard resilience framework in .NET 8+) offers a clean, production-grade pattern to protect your microservices against cascading failures using the Circuit Breaker pattern.

The Problem: Cascading HTTP Failures

Imagine an Order Microservice calling a Payment Microservice via HTTP. If the Payment service goes down or experiences a database deadlock, incoming requests to the Order service will hang while waiting for responses.

Without a circuit breaker:

  1. Every new order request allocates a thread to call the failing Payment API.

  2. Threads quickly pile up, consuming CPU and RAM.

  3. The Order service eventually runs out of thread-pool resources, crashing entirely—even for endpoints that don't depend on the Payment service.

The Solution: The Circuit Breaker Pattern

A Circuit Breaker monitors outgoing HTTP traffic and automatically short-circuits requests when a dependency fails repeatedly:

Step-by-step Implementation

1. Define the Refit Client Interface

Refit eliminates boilerplate HTTP code by mapping C# interfaces directly to HTTP endpoints:

using Refit;

public record PaymentRequest(string OrderId, decimal Amount);
public record PaymentResponse(string TransactionId, string Status);

public interface IPaymentApi
{
    [Post("/api/v1/payments")]
    Task<PaymentResponse> ProcessPaymentAsync([Body] PaymentRequest request);
}

2. Configure Refit with Polly Resilience Pipelines

In .NET 8+, the Microsoft.Extensions.Http.Resilience package allows you to attach Polly v8 pipelines directly to Refit clients during Dependency Injection setup using AddResilienceHandler.

using System.Net;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Polly;
using Polly.CircuitBreaker;
using Refit;

var builder = Host.CreateApplicationBuilder(args);

// Register the Refit Client with a Circuit Breaker Pipeline
builder.Services.AddRefitClient<IPaymentApi>()
    .ConfigureHttpClient(client =>
    {
        client.BaseAddress = new Uri("https://api.paymentservice.com");
        client.Timeout = TimeSpan.FromSeconds(5);
    })
    .AddResilienceHandler("RefitPaymentCircuitBreaker", resilienceBuilder =>
    {
        resilienceBuilder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
        {
            // Open the circuit if 50% or more calls fail in the sampling window
            FailureRatio = 0.5,

            // Minimum throughput required in the window before rules apply
            MinimumThroughput = 5,

            // Time window over which failure rate is calculated
            SamplingDuration = TimeSpan.FromSeconds(10),

            // Time the circuit stays OPEN before entering HALF-OPEN state
            BreakDuration = TimeSpan.FromSeconds(15),

            // Define what errors trigger the circuit breaker
            ShouldHandle = new HttpResiliencePredicateBuilder()
                .HandleStatusCode(HttpStatusCode.InternalServerError)
                .HandleStatusCode(HttpStatusCode.ServiceUnavailable)
                .HandleStatusCode(HttpStatusCode.RequestTimeout)
                .Build(),

            // Monitoring and Observability Callbacks
            OnOpened = args =>
            {
                Console.WriteLine($"[CIRCUIT OPEN] Payment API unreachable. Short-circuiting traffic for {args.BreakDuration.TotalSeconds}s.");
                return ValueTask.CompletedTask;
            },
            OnClosed = args =>
            {
                Console.WriteLine("[CIRCUIT CLOSED] Payment API recovered. Resuming normal operations.");
                return ValueTask.CompletedTask;
            },
            OnHalfOpened = args =>
            {
                Console.WriteLine("[CIRCUIT HALF-OPEN] Sending trial requests to verify recovery...");
                return ValueTask.CompletedTask;
            }
        });
    });

3. Consume the Refit Client Safely with Fallback Handling

When the circuit is Open, calls to IPaymentApi throw a BrokenCircuitException. Handle this gracefully in your application logic to provide fallback behavior (e.g., queueing the order for processing later):

public class OrderService
{
    private readonly IPaymentApi _paymentApi;

    public OrderService(IPaymentApi paymentApi)
    {
        _paymentApi = paymentApi;
    }

    public async Task<bool> CheckoutAsync(string orderId, decimal amount)
    {
        try
        {
            var response = await _paymentApi.ProcessPaymentAsync(new PaymentRequest(orderId, amount));
            return response.Status == "APPROVED";
        }
        catch (BrokenCircuitException)
        {
            // Circuit is OPEN - Service is down, short-circuit locally
            Console.WriteLine($"Order {orderId}: Payment service unavailable. Queueing for offline execution.");
            await SaveToOfflineQueueAsync(orderId, amount);
            return false;
        }
        catch (ApiException ex)
        {
            // Handle HTTP response error codes (4xx, 5xx)
            Console.WriteLine($"Order {orderId}: API returned error: {ex.StatusCode}");
            return false;
        }
    }

    private Task SaveToOfflineQueueAsync(string orderId, decimal amount)
    {
        // Outbox/Queue logic here
        return Task.CompletedTask;
    }
}

Best Practices for Refit + Circuit Breakers

  1. Combine Retry with Circuit Breaker: Layer a Retry policy with exponential backoff and jitter inside the Circuit Breaker. Let short transient glitches self-heal via retries before blowing the breaker.

  2. Set Sensible Timeouts: Always configure HttpClient.Timeout or use Polly's AttemptTimeout. Uncapped HTTP requests will hold connections open indefinitely, preventing the circuit breaker from evaluating metrics promptly.

  3. Log & Export Metrics: Wire up the OnOpened and OnClosed delegates to your logging framework (e.g., Serilog, OpenTelemetry) to trigger alerts when critical dependencies fail.