Skip to content

Circuit breaker

When a dependency fails, continuing to call it on every request can overwhelm the service and exacerbate the outage. A circuit breaker prevents this by stopping calls to the dependency, allowing it time to recover, and periodically letting a small number of trial calls through to test for recovery.

Circuit breakers are opt-in and are implemented as objects rather than settings. This allows you to define the scope of the "stop calling" decision based on your application's architecture.

Enable the circuit breaker

Create a Breaker object and assign it to your policy.

csharp
// Breaker scope is a variable with a name and a lifetime. `with` copies the reference,
// so every policy derived from `payments` shares this breaker.
var breaker = new Breaker { Name = "payments" };

var payments = Resilience.Http with { Breaker = breaker };
var paymentsWrites = payments with { Attempts = 1 };

The scope of the breaker is determined by where you hold the reference. Because the with keyword copies the reference and not the state, two policies derived from a common ancestor share the same breaker.

For HTTP calls, the handler can scope a breaker per host automatically. For more information, see per-host scope.

Breaker states

A circuit breaker is always in one of these four states:

StateDescription
ClosedNormal operation. Calls are allowed, and failures are tracked.
OpenThe dependency is failing. Calls are refused for the duration of the break.
HalfOpenThe break duration has elapsed. A small number of trial calls (probes) are allowed to test for recovery.
IsolatedForced open by an operator via Isolate(). The breaker stays open until Reset() is called.

Trip conditions

A circuit breaker can trip based on consecutive failures or based on rates of failure and slowness. For example, a dependency might return successful responses but with such high latency that it exhausts your thread and connection pools.

SettingDefaultDescription
ConsecutiveFailures5The number of consecutive failures before the breaker opens.
FailureRationullAn optional rate-based trip condition, evaluated alongside the consecutive failure counter.
MinimumCalls20The minimum number of calls required before a ratio-based trip is evaluated.
Window30 sThe sliding window over which rates are measured.
SlowCallThresholdnullThe latency threshold; any attempt slower than this counts as a slow call.
SlowCallRatio0.5The proportion of slow calls within the window that trips the breaker.
BreakDuration15 sThe duration of the first break.
MaxBreakDuration2 minThe maximum break duration. The break duration doubles on each consecutive open.
HalfOpenProbes1The number of concurrent trial calls allowed while in the HalfOpen state.
ProbeSuccesses2The number of successful probes required to close the breaker.
csharp
// The most common real degradation is not errors, it is a dependency answering 200s at
// 30x normal latency. A breaker that only counts errors stays closed through the whole
// incident, because the responses are not failing - they are just slow.
var breaker = new Breaker(settings: new BreakerSettings
{
    ConsecutiveFailures = 5, // the default trip condition
    SlowCallThreshold = TimeSpan.FromSeconds(value: 2), // anything slower counts against
    SlowCallRatio = 0.5, // half the window being slow trips it
    MinimumCalls = 20, // below this, a ratio means nothing
    Window = TimeSpan.FromSeconds(value: 30),
    BreakDuration = TimeSpan.FromSeconds(value: 15), // doubles per consecutive open
    MaxBreakDuration = TimeSpan.FromMinutes(value: 2),
    ProbeSuccesses = 2, // two good probes to close, not one
})
{
    Name = "search",
};

The breaker samples individual attempts. Only Transient outcomes count as evidence of failure. Throttled responses indicate the dependency is functioning and defending itself, and Permanent outcomes are typically client-side issues.

Handle refused calls

When a circuit breaker refuses a call, it serves a short pause before returning. This prevents callers in tight polling loops from busy-spinning and wasting CPU.

csharp
// A refused call reports itself rather than the dependency's last exception, and it says
// which guard refused it. RetryAfter is there so a caller that schedules its own polling
// does not have to guess.
if (result.Exception is CallRejectedException rejection)
{
    Console.WriteLine(value: rejection.Reason); // DependencyUnavailable, or BudgetExhausted
    Console.WriteLine(value: rejection.RetryAfter); // when to come back, when there is an answer
}

The breaker uses StopReason.DependencyUnavailable for refusals. The retry budget uses BudgetExhausted.

For more information, see Guarded rejection.

Manage the breaker

You can monitor the state of the breaker or manually control its behavior.

csharp
var state = breaker.State; // Closed, Open, HalfOpen or Isolated
var since = breaker.OpenedAt; // null while it is closed

breaker.Isolate(); // force it open and keep it there
breaker.Reset(); // close it and forget the history

State reports HalfOpen for an open breaker whose break duration has elapsed. Reading the state does not consume a probe slot. Isolate and Reset do not raise events because they are not triggered by a call.

Transitions trigger BreakerOpened, BreakerClosed, and BreakerHalfOpened events on the call that caused the transition.

For a deeper dive, see Breaker internals.

Released under the MIT License.