Skip to content
Buy me a coffee

Circuit breaker

A circuit breaker watches the failures of one dependency. When they cross a threshold it opens: calls fail at once with BrokenCircuitError instead of piling up on a service that can’t answer. After a break it lets a probe through, and closes again if the probe succeeds.

Use one breaker per downstream dependency, shared by every call to it (build the pipeline once, or use the registry). A breaker created per request never sees enough failures to open. Don’t use it for errors that say nothing about the dependency’s health, such as validation errors; the default predicate already ignores 4xx.

import {
class ResiliencePipelineBuilder<T = any, E = never, C extends string = never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
} from '@ylcnfrht/resilience';
const
const payments: ResiliencePipeline<any, BrokenCircuitError>
payments
= new
new ResiliencePipelineBuilder<any, never, never>(options?: ResiliencePipelineBuilderOptions): ResiliencePipelineBuilder<any, never, never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
()
.
ResiliencePipelineBuilder<any, never, never>.addCircuitBreaker(options?: CircuitBreakerOptions<any>): Next<any, never, never, BrokenCircuitError>
addCircuitBreaker
({
CircuitBreakerOptions<any>.failureRatio?: number | undefined

Failure ratio in the window that breaks the circuit. Default 0.1.

failureRatio
: 0.5, // open when half the calls fail…
CircuitBreakerOptions<any>.minimumThroughput?: number | undefined

Minimum recorded calls in the window before the ratio is considered. Default 100.

minimumThroughput
: 20, // …once at least 20 calls were seen…
CircuitBreakerOptions<any>.samplingDuration?: Duration | undefined

Time-based window: calls of the last samplingDuration. Default 30 s. Ignored when samplingCount is set.

samplingDuration
: '30s', // …in the last 30 s
CircuitBreakerOptions<any>.breakDuration?: Duration | undefined

How long the circuit stays open. Default 5 s.

breakDuration
: '10s', // stay open for 10 s, then probe
})
.
ResiliencePipelineBuilder<any, BrokenCircuitError, never>.build(): ResiliencePipeline<any, BrokenCircuitError>
build
();
const
const res: Response
res
= await
const payments: ResiliencePipeline<any, BrokenCircuitError>
payments
.
ResiliencePipeline<any, BrokenCircuitError>.execute<Response>(callback: (context: ResilienceContext) => MaybePromise<Response>, options?: ExecuteOptions): Promise<Response>

Runs the callback through the pipeline; throws the final error.

execute
(({
signal: AbortSignal

Aborted when the caller cancels, or when a strategy (timeout, hedging) gives up on this execution.

signal
}) =>
function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> (+1 overload)
fetch
('https://pay.example.com/charge', {
RequestInit.signal?: AbortSignal | null | undefined

An AbortSignal to set request's signal.

signal
}));

For low traffic, consecutiveFailures is simpler: addCircuitBreaker({ consecutiveFailures: 5 }) opens after 5 failures in a row.

failure rate ≥ 50%after 3 sprobe failsprobe succeedsClosedcalls flowOpencalls rejectedHalf-openone probe

Circuit closed. 0 succeeded, 0 failed, 0 rejected.

Stateclosed
Window failure rate0%
Calls in window0/10
Half-open in–
Succeeded / failed0 / 0
Rejected (open)0

Opens when at least 50% of the last 10 calls failed (and at least 4 calls were recorded). While open, calls fail fast with BrokenCircuitError; after 3 s the next call is let through as a probe.

State Calls Leaves when
closed Pass through. Each outcome is recorded as a success, a failure or ignored. The opening rule fires after a failure → open.
open Rejected with BrokenCircuitError (retryAfter = ms left). The break is over and a call arrives → half-open. There is no timer: the next call triggers it.
half-open Up to halfOpenMaxCalls probes pass; others are rejected (retryAfter: 0). All probes finished → closed if they were healthy enough, else open again. A probe slower than halfOpenMaxWait counts as failed → open.
isolated Rejected with IsolatedCircuitError. Manual close only.

What counts. An outcome is a failure when shouldHandle matches it. The default matches errors except 4xx (408 and 429 do count), aborts and rejections. Successes count as successes. Everything else, such as a 404, is ignored: neither a failure nor a success. Rejections from other strategies (a full limiter, another open circuit) are never counted, whatever shouldHandle says.

Stale calls. A call that started before the circuit changed state can’t affect the new state when it finishes. A slow failure from before a reset won’t reopen the circuit.

Probes. With the default halfOpenMaxCalls: 1, one probe decides. With more, the circuit closes when the probes’ failure ratio stays below failureRatio, or, with halfOpenSuccessThreshold, when at least that fraction succeeded. An ignored probe outcome frees its slot for another probe.

Pick one rule; consecutiveFailures can be combined with the others.

Rule Options Opens when Good for
Ratio, time window (default) failureRatio, minimumThroughput, samplingDuration, minimumRps total ≥ minimumThroughput and failures / total ≥ failureRatio over the last samplingDuration Steady, high traffic.
Ratio, count window failureRatio, minimumThroughput, samplingCount Same, over the last samplingCount calls Low or bursty traffic.
Consecutive consecutiveFailures That many failures in a row (a success resets the count) Low traffic, simple setups.
Custom breaker: CircuitBreakerPolicy Your shouldOpen(now) returns true Anything else.
  • Setting consecutiveFailures without failureRatio turns the ratio rule off. Set both and either one opens the circuit.
  • The time window is kept in 10 buckets, so old calls drop out in steps of samplingDuration / 10.
  • minimumRps (time window only) also requires that many calls per second on average over the window, so a quiet period’s few failures can’t open the circuit.
import {
class ResiliencePipelineBuilder<T = any, E = never, C extends string = never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
} from '@ylcnfrht/resilience';
// The last 20 calls; open at 50% failures once 10 were recorded.
const
const pipeline: ResiliencePipeline<any, BrokenCircuitError>
pipeline
= new
new ResiliencePipelineBuilder<any, never, never>(options?: ResiliencePipelineBuilderOptions): ResiliencePipelineBuilder<any, never, never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
()
.
ResiliencePipelineBuilder<any, never, never>.addCircuitBreaker(options?: CircuitBreakerOptions<any>): Next<any, never, never, BrokenCircuitError>
addCircuitBreaker
({
CircuitBreakerOptions<any>.samplingCount?: number | undefined

Count-based window: the last samplingCount calls. Better for low or bursty traffic.

samplingCount
: 20,
CircuitBreakerOptions<any>.minimumThroughput?: number | undefined

Minimum recorded calls in the window before the ratio is considered. Default 100.

minimumThroughput
: 10,
CircuitBreakerOptions<any>.failureRatio?: number | undefined

Failure ratio in the window that breaks the circuit. Default 0.1.

failureRatio
: 0.5 })
.
ResiliencePipelineBuilder<any, BrokenCircuitError, never>.build(): ResiliencePipeline<any, BrokenCircuitError>
build
();

A CircuitBreakerPolicy replaces failureRatio, minimumThroughput and the window (consecutiveFailures still applies). Only calls made while closed are recorded. Use one instance per breaker; it holds that breaker’s history.

import {
class ResiliencePipelineBuilder<T = any, E = never, C extends string = never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
, type
(alias) interface CircuitBreakerPolicy
import CircuitBreakerPolicy

Decides when a closed circuit opens (cockatiel's IBreaker). Only calls made while closed are recorded; now is the pipeline's time provider. One instance per breaker: it holds that breaker's history.

CircuitBreakerPolicy
} from '@ylcnfrht/resilience';
/** Open after 5 failures within one minute, whatever the success count. */
class
class FailuresPerMinute

Open after 5 failures within one minute, whatever the success count.

FailuresPerMinute
implements
(alias) interface CircuitBreakerPolicy
import CircuitBreakerPolicy

Decides when a closed circuit opens (cockatiel's IBreaker). Only calls made while closed are recorded; now is the pipeline's time provider. One instance per breaker: it holds that breaker's history.

CircuitBreakerPolicy
{
#failures: number[] = [];
FailuresPerMinute.record(failed: boolean, now: number): void
record
(
failed: boolean
failed
: boolean,
now: number
now
: number) {
if (
failed: boolean
failed
) this.#failures.
Array<number>.push(...items: number[]): number

Appends new elements to the end of an array, and returns the new length of the array.

@param ― items New elements to add to the array.

push
(
now: number
now
);
}
FailuresPerMinute.shouldOpen(now: number): boolean

Asked after each recorded failure (outside warm-up).

shouldOpen
(
now: number
now
: number) {
this.#failures = this.#failures.
Array<number>.filter(predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): number[] (+1 overload)

Returns the elements of an array that meet the condition specified in a callback function.

@param ― predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.

@param ― thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.

filter
((
t: number
t
) =>
now: number
now
-
t: number
t
< 60_000);
return this.#failures.
Array<number>.length: number

Gets or sets the length of the array. This is a number one higher than the highest index in the array.

length
>= 5;
}
FailuresPerMinute.reset(): void

Called when the circuit closes.

reset
() {
this.#failures = [];
}
}
const
const pipeline: ResiliencePipeline<any, BrokenCircuitError>
pipeline
= new
new ResiliencePipelineBuilder<any, never, never>(options?: ResiliencePipelineBuilderOptions): ResiliencePipelineBuilder<any, never, never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
().
ResiliencePipelineBuilder<any, never, never>.addCircuitBreaker(options?: CircuitBreakerOptions<any>): Next<any, never, never, BrokenCircuitError>
addCircuitBreaker
({
CircuitBreakerOptions<any>.breaker?: CircuitBreakerPolicy | undefined

Custom rule for when a closed circuit opens; replaces failureRatio, minimumThroughput and the sampling window (consecutiveFailures, if set, still applies too).

breaker
: new
constructor FailuresPerMinute(): FailuresPerMinute

Open after 5 failures within one minute, whatever the success count.

FailuresPerMinute
() }).
ResiliencePipelineBuilder<any, BrokenCircuitError, never>.build(): ResiliencePipeline<any, BrokenCircuitError>
build
();
Method Required Called
record(failed, now) yes For every success or failure while closed. now comes from the pipeline’s time provider.
shouldOpen(now) yes After each recorded failure, outside warm-up.
reset() yes When the circuit closes.
metrics(now) no For stateProvider.metrics and breakDurationGenerator. Zeros when absent.
snapshot() no JSON history stored as CircuitSnapshot.window.
restore(state) no Receives initialState.window. It may come from another policy or be malformed: validate it.

breakDuration (default 5 s) is how long the circuit stays open. For a break that grows while the dependency stays down, use breakDurationGenerator. It receives the window metrics (total, failureCount, failureRate), halfOpenAttempts (failed half-open rounds in a row) and context:

import {
class ResiliencePipelineBuilder<T = any, E = never, C extends string = never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
} from '@ylcnfrht/resilience';
// 5 s, 10 s, 20 s, … up to 2 min, reset when the circuit closes.
const
const pipeline: ResiliencePipeline<any, BrokenCircuitError>
pipeline
= new
new ResiliencePipelineBuilder<any, never, never>(options?: ResiliencePipelineBuilderOptions): ResiliencePipelineBuilder<any, never, never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
()
.
ResiliencePipelineBuilder<any, never, never>.addCircuitBreaker(options?: CircuitBreakerOptions<any>): Next<any, never, never, BrokenCircuitError>
addCircuitBreaker
({
CircuitBreakerOptions<any>.consecutiveFailures?: number | undefined

Break after this many failures in a row (a success resets the count). Simple and good for low traffic. When set without failureRatio, the ratio check is off and only this rule applies.

consecutiveFailures
: 5,
CircuitBreakerOptions<any>.breakDurationGenerator?: ((args: BreakDurationArgs) => MaybePromise<Duration>) | undefined
breakDurationGenerator
: ({
halfOpenAttempts: number

Consecutive half-open rounds that failed; use it for growing break durations.

halfOpenAttempts
}) =>
var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.
Math.min(...values: number[]): number

Returns the smaller of a set of supplied numeric expressions.

@param ― values Numeric expressions to be evaluated.

min
(5_000 * 2 **
halfOpenAttempts: number

Consecutive half-open rounds that failed; use it for growing break durations.

halfOpenAttempts
, 120_000),
})
.
ResiliencePipelineBuilder<any, BrokenCircuitError, never>.build(): ResiliencePipeline<any, BrokenCircuitError>
build
();

If the generator throws or returns an invalid duration, breakDuration is used instead.

Option Type Default Description
name string 'CircuitBreaker' Strategy name in telemetry and getStrategy().
shouldHandle (args) => boolean (or a promise) or PredicateBuilder transient errors Which outcomes are failures. Non-matching errors are ignored, not successes.
failureRatio number in (0, 1] 0.1 Failure ratio that opens the circuit. Also the probe threshold in half-open.
consecutiveFailures integer ≥ 1 none Open after this many failures in a row. Alone, it disables the ratio rule.
minimumThroughput integer ≥ 1 100 Calls in the window before the ratio counts.
minimumRps number > 0 none Time window only: minimum average calls per second before the ratio counts.
samplingDuration Duration 30000 (30 s) Time window length. Ignored when samplingCount is set.
samplingCount integer ≥ 1 none Count window: the last N recorded calls.
breakDuration Duration 5000 (5 s) How long the circuit stays open. Must be > 0.
breakDurationGenerator (args: BreakDurationArgs) => Duration (or a promise) none Per-opening break duration.
halfOpenMaxCalls integer ≥ 1 1 Probes let through while half-open.
halfOpenSuccessThreshold number in (0, 1] none Fraction of finished probes that must succeed to close. 0.8 with 5 probes needs 4.
halfOpenMaxWait Duration 30000 (30 s) A probe running longer counts as failed and reopens the circuit.
warmUpDuration Duration 0 After build(), failures are recorded but can’t open the circuit for this long.
breaker CircuitBreakerPolicy built-in window Custom opening rule, see above.
manualControl CircuitBreakerManualControl none Isolate or close by hand.
stateProvider CircuitBreakerStateProvider none Read state, metrics and snapshots.
initialState CircuitSnapshot none Resume from a saved snapshot.
extensions CircuitBreakerExtension[] none Background behaviour: health checks, shared state.
onOpened (args: OnCircuitOpenedArgs) => void none outcome, breakDuration, isManual, context.
onClosed (args: OnCircuitClosedArgs) => void none outcome, isManual, context.
onHalfOpened ({ context }) => void none The first call after the break.
onRejected (args: OnCircuitRejectedArgs) => void none Every refused call: context, state, retryAfter. The “short-circuited” count.

Invalid values throw RangeError on build(). Callbacks may be async and are awaited.

CircuitBreakerManualControl holds circuits open (isolated) or closes them, for example from an admin endpoint during an incident. One control can drive several breakers.

import {
class ResiliencePipelineBuilder<T = any, E = never, C extends string = never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
,
class CircuitBreakerManualControl

Manually isolate (hold open) or close one or more circuit breakers, e.g. from an admin endpoint or a health check.

CircuitBreakerManualControl
} from '@ylcnfrht/resilience';
const
const control: CircuitBreakerManualControl
control
= new
new CircuitBreakerManualControl(isIsolated?: boolean): CircuitBreakerManualControl

Manually isolate (hold open) or close one or more circuit breakers, e.g. from an admin endpoint or a health check.

CircuitBreakerManualControl
(); // `new CircuitBreakerManualControl(true)` starts isolated
const
const pipeline: ResiliencePipeline<any, BrokenCircuitError>
pipeline
= new
new ResiliencePipelineBuilder<any, never, never>(options?: ResiliencePipelineBuilderOptions): ResiliencePipelineBuilder<any, never, never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
().
ResiliencePipelineBuilder<any, never, never>.addCircuitBreaker(options?: CircuitBreakerOptions<any>): Next<any, never, never, BrokenCircuitError>
addCircuitBreaker
({
CircuitBreakerOptions<any>.manualControl?: CircuitBreakerManualControl | undefined
manualControl
:
const control: CircuitBreakerManualControl
control
}).
ResiliencePipelineBuilder<any, BrokenCircuitError, never>.build(): ResiliencePipeline<any, BrokenCircuitError>
build
();
const
const releaseDeploy: () => Promise<void>
releaseDeploy
= await
const control: CircuitBreakerManualControl
control
.
CircuitBreakerManualControl.isolate(): Promise<() => Promise<void>>

Holds the circuits isolated until every isolation is released (stacking) or close() is called. Resolves to the release function of this isolation; releasing twice, or after close(), does nothing.

isolate
();
const
const releaseIncident: () => Promise<void>
releaseIncident
= await
const control: CircuitBreakerManualControl
control
.
CircuitBreakerManualControl.isolate(): Promise<() => Promise<void>>

Holds the circuits isolated until every isolation is released (stacking) or close() is called. Resolves to the release function of this isolation; releasing twice, or after close(), does nothing.

isolate
();
await
const releaseDeploy: () => Promise<void>
releaseDeploy
(); // still isolated: the incident hold remains
await
const releaseIncident: () => Promise<void>
releaseIncident
(); // last hold released → closed
const control: CircuitBreakerManualControl
control
.
CircuitBreakerManualControl.isIsolated: boolean
isIsolated
; // false
await
const control: CircuitBreakerManualControl
control
.
CircuitBreakerManualControl.close(): Promise<void>

Closes the circuits now, dropping every outstanding isolation.

close
(); // closes at once and drops every outstanding hold

Isolations stack: the circuit stays isolated until every isolate() has been released, or close() is called. Releasing twice does nothing. While isolated, trip() and states adopted from sharedCircuitState are ignored.

import {
class ResiliencePipelineBuilder<T = any, E = never, C extends string = never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
,
class CircuitBreakerStateProvider

Reads the state and window metrics of a single circuit breaker (health checks, dashboards, persistence).

CircuitBreakerStateProvider
, type
class CircuitBreakerStrategy<T>
CircuitBreakerStrategy
} from '@ylcnfrht/resilience';
const
const state: CircuitBreakerStateProvider
state
= new
new CircuitBreakerStateProvider(): CircuitBreakerStateProvider

Reads the state and window metrics of a single circuit breaker (health checks, dashboards, persistence).

CircuitBreakerStateProvider
();
const
const pipeline: ResiliencePipeline<any, BrokenCircuitError>
pipeline
= new
new ResiliencePipelineBuilder<any, never, never>(options?: ResiliencePipelineBuilderOptions): ResiliencePipelineBuilder<any, never, never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
().
ResiliencePipelineBuilder<any, never, never>.addCircuitBreaker(options?: CircuitBreakerOptions<any>): Next<any, never, never, BrokenCircuitError>
addCircuitBreaker
({
CircuitBreakerOptions<any>.stateProvider?: CircuitBreakerStateProvider | undefined
stateProvider
:
const state: CircuitBreakerStateProvider
state
}).
ResiliencePipelineBuilder<any, BrokenCircuitError, never>.build(): ResiliencePipeline<any, BrokenCircuitError>
build
();
const state: CircuitBreakerStateProvider
state
.
CircuitBreakerStateProvider.circuitState: CircuitState
circuitState
; // 'closed' | 'open' | 'half-open' | 'isolated'
const state: CircuitBreakerStateProvider
state
.
CircuitBreakerStateProvider.metrics: CircuitMetrics
metrics
; // { total, failureCount, failureRate } of the current window
const state: CircuitBreakerStateProvider
state
.
CircuitBreakerStateProvider.isWarmingUp: boolean

True during warmUpDuration, while failures are recorded but can't open the circuit (opossum's warmUp).

isWarmingUp
; // true during warmUpDuration
const state: CircuitBreakerStateProvider
state
.
CircuitBreakerStateProvider.snapshot(): CircuitSnapshot

Current state, rolling window included, as plain JSON; pass it back as initialState to resume.

snapshot
(); // plain JSON, see Persistence
// Or the live strategy itself:
const
const breaker: CircuitBreakerStrategy<any> | undefined
breaker
=
const pipeline: ResiliencePipeline<any, BrokenCircuitError>
pipeline
.
ResiliencePipeline<any, BrokenCircuitError>.getStrategy<CircuitBreakerStrategy<any>>(nameOrKind: string): CircuitBreakerStrategy<any> | undefined

The strategy instance named name (or of class kind), to read its live state: getStrategy<CircuitBreakerStrategy>('CircuitBreaker')?.state.

getStrategy
<
class CircuitBreakerStrategy<T>
CircuitBreakerStrategy
<any>>('CircuitBreaker');
const breaker: CircuitBreakerStrategy<any> | undefined
breaker
?.
CircuitBreakerStrategy<any>.lastFailure: Outcome<any> | undefined

The last outcome counted as a failure, if any.

lastFailure
; // the last outcome counted as a failure
await
const breaker: CircuitBreakerStrategy<any> | undefined
breaker
?.
CircuitBreakerStrategy<any>.trip(): Promise<void>

Open the circuit now for its normal break duration (unlike isolate, it half-opens by itself afterwards).

trip
(); // open now for the break duration; half-opens by itself afterwards
await
const breaker: CircuitBreakerStrategy<any> | undefined
breaker
?.
CircuitBreakerStrategy<any>.reset(): Promise<void>

Close the circuit now and clear its window.

reset
(); // close now and clear the window

A state provider belongs to one breaker; attaching it to a second one throws. getStrategy() takes the strategy’s name (default 'CircuitBreaker') or its class name. The registry lists every breaker with registry.circuitBreakers().

Serverless functions and restarts lose in-memory state. Save stateProvider.snapshot() and pass it back as initialState. The snapshot includes the rolling window, so the failure history survives too.

const
const state: CircuitBreakerStateProvider
state
= new
new CircuitBreakerStateProvider(): CircuitBreakerStateProvider

Reads the state and window metrics of a single circuit breaker (health checks, dashboards, persistence).

CircuitBreakerStateProvider
();
const
const save: () => Promise<void>
save
= () =>
const kv: {
get(key: string): Promise<CircuitSnapshot | null>;
put(key: string, value: string): Promise<void>;
}
kv
.
function put(key: string, value: string): Promise<void>
put
('breaker:payments',
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@param ― value A JavaScript value, usually an object or array, to be converted.

@param ― replacer A function that transforms the results.

@param ― space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

@throws ― {TypeError} If a circular reference or a BigInt value is found.

stringify
(
const state: CircuitBreakerStateProvider
state
.
CircuitBreakerStateProvider.snapshot(): CircuitSnapshot

Current state, rolling window included, as plain JSON; pass it back as initialState to resume.

snapshot
()));
const
const pipeline: ResiliencePipeline<any, BrokenCircuitError>
pipeline
= new
new ResiliencePipelineBuilder<any, never, never>(options?: ResiliencePipelineBuilderOptions): ResiliencePipelineBuilder<any, never, never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
()
.
ResiliencePipelineBuilder<any, never, never>.addCircuitBreaker(options?: CircuitBreakerOptions<any>): Next<any, never, never, BrokenCircuitError>
addCircuitBreaker
({
CircuitBreakerOptions<any>.stateProvider?: CircuitBreakerStateProvider | undefined
stateProvider
:
const state: CircuitBreakerStateProvider
state
,
CircuitBreakerOptions<any>.initialState?: CircuitSnapshot | undefined

Resume from a snapshot taken with stateProvider.snapshot(), e.g. after a serverless cold start or a restart.

initialState
: (await
const kv: {
get(key: string): Promise<CircuitSnapshot | null>;
put(key: string, value: string): Promise<void>;
}
kv
.
function get(key: string): Promise<CircuitSnapshot | null>
get
('breaker:payments')) ??
var undefined
undefined
,
CircuitBreakerOptions<any>.onOpened?: ((args: OnCircuitOpenedArgs<any>) => MaybePromise<void>) | undefined
onOpened
:
const save: () => Promise<void>
save
,
CircuitBreakerOptions<any>.onClosed?: ((args: OnCircuitClosedArgs<any>) => MaybePromise<void>) | undefined
onClosed
:
const save: () => Promise<void>
save
,
})
.
ResiliencePipelineBuilder<any, BrokenCircuitError, never>.build(): ResiliencePipeline<any, BrokenCircuitError>
build
();
CircuitSnapshot field Meaning
state 'closed' | 'open' | 'half-open' | 'isolated'. A saved half-open resumes as open and due for a probe (the probe in flight is gone).
openUntil Epoch ms when an open circuit lets a probe through.
consecutiveFailures, halfOpenAttempts Counters.
updatedAt Epoch ms of the last state change; the newest wins in a shared store.
window The policy’s history. Malformed data is skipped.

Extensions add background behaviour. They live in separate modules, so you only bundle what you use. pipeline.dispose() stops them.

const
const store: MemoryCircuitStateStore
store
= new
new MemoryCircuitStateStore(): MemoryCircuitStateStore

In-process CircuitStateStore: shares one breaker state between pipelines in the same process (and in tests).

MemoryCircuitStateStore
(); // RedisCircuitStateStore across servers
const
const pipeline: ResiliencePipeline<any, BrokenCircuitError>
pipeline
= new
new ResiliencePipelineBuilder<any, never, never>(options?: ResiliencePipelineBuilderOptions): ResiliencePipelineBuilder<any, never, never>

Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), … Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.

The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().

ResiliencePipelineBuilder
()
.
ResiliencePipelineBuilder<any, never, never>.addCircuitBreaker(options?: CircuitBreakerOptions<any>): Next<any, never, never, BrokenCircuitError>
addCircuitBreaker
({
CircuitBreakerOptions<any>.consecutiveFailures?: number | undefined

Break after this many failures in a row (a success resets the count). Simple and good for low traffic. When set without failureRatio, the ratio check is off and only this rule applies.

consecutiveFailures
: 5,
CircuitBreakerOptions<any>.extensions?: CircuitBreakerExtension[] | undefined

Background behaviour plugged into the breaker, e.g. circuitHealthCheck({ check, interval }) or sharedCircuitState({ store, key }) from this package. Kept out of the core breaker so you only bundle what you use.

extensions
: [
function circuitHealthCheck(options: CircuitHealthCheckOptions): CircuitBreakerExtension

Probes the dependency in the background while the circuit is closed; a failing probe opens it (opossum's healthCheck). addCircuitBreaker({ extensions: [circuitHealthCheck({ check: () => db.ping(), interval: '10s' })] })

circuitHealthCheck
({
CircuitHealthCheckOptions.check: (signal: AbortSignal) => MaybePromise<unknown>

Resolve (anything but false) when healthy; throw, reject or return false when not.

check
: (
signal: AbortSignal
signal
) =>
const db: {
ping(signal: AbortSignal): Promise<void>;
}
db
.
function ping(signal: AbortSignal): Promise<void>
ping
(
signal: AbortSignal
signal
),
CircuitHealthCheckOptions.interval: Duration
interval
: '10s' }),
function sharedCircuitState(options: SharedCircuitStateOptions): CircuitBreakerExtension

Shares the breaker's state with other instances (servers, processes) through a store, e.g. RedisCircuitStateStore: one instance seeing the dependency fail opens the circuit for all of them. addCircuitBreaker({ extensions: [sharedCircuitState({ store, key: 'payments' })] })

sharedCircuitState
({
SharedCircuitStateOptions.store: CircuitStateStore
store
,
SharedCircuitStateOptions.key: string

One key per logical breaker (e.g. per downstream service).

key
: 'payments-db' }),
],
})
.
ResiliencePipelineBuilder<any, BrokenCircuitError, never>.build(): ResiliencePipeline<any, BrokenCircuitError>
build
();
// on shutdown
const pipeline: ResiliencePipeline<any, BrokenCircuitError>
pipeline
.
ResiliencePipeline<any, BrokenCircuitError>.dispose(): void

Stops background work (health checks, store sync timers). Later executions fail.

dispose
();

circuitHealthCheck probes the dependency every interval while the circuit is closed. A check that throws, rejects, returns false or takes longer than timeout opens the circuit for its break duration (reported as OnHealthCheckFailed). Its timers don’t keep the process alive.

Option Type Default
check (signal: AbortSignal) => unknown (or a promise) required
interval Duration required
timeout Duration interval

sharedCircuitState syncs the state with other instances through a CircuitStateStore: one instance seeing the dependency fail opens the circuit for all of them. It writes on every local state change and reads at start and every syncInterval. The newest updatedAt wins. The rolling window stays local: each instance judges its own traffic.

Option Type Default
store CircuitStateStore (get(key), set(key, snapshot)) required
key string, one per logical breaker required
syncInterval Duration '5s'

Stores: MemoryCircuitStateStore (one process, tests) and RedisCircuitStateStore from @ylcnfrht/resilience/redis (see Multi-instance with Redis). A failing store never fails calls; it is reported as OnCircuitStoreError.

Telemetry event Callback When
OnCircuitOpened onOpened Opened by the rule, a failed probe, trip(), a health check (isManual: false) or isolated (isManual: true, breakDuration: Infinity).
OnCircuitClosed onClosed Closed after healthy probes (isManual: false), or by reset() / manual control (isManual: true).
OnCircuitHalfOpened onHalfOpened The first call after the break.
OnCircuitRejected onRejected Every call refused while open, half-open (slots full) or isolated.
OnHalfOpenProbeTimedOut none A probe ran longer than halfOpenMaxWait.
OnHealthCheckFailed none A circuitHealthCheck failed.
OnCircuitStateSynced none sharedCircuitState adopted a newer remote state.
OnCircuitStoreError none The shared store threw.
Error code HTTP Notes
BrokenCircuitError CIRCUIT_OPEN 503 retryAfter: ms until a probe is allowed (0 while half-open slots are taken). cause: the last failure’s error.
IsolatedCircuitError CIRCUIT_ISOLATED 503 Extends BrokenCircuitError; no retryAfter.

Both are rejections (isRejection(e) is true, guard: isBrokenCircuitError(e)): retry never retries them, other breakers don’t count them, and the default fallback handles them. With typed errors, Outcome.match needs CIRCUIT_OPEN and CIRCUIT_ISOLATED branches.

  • Defaults that never open on modest traffic. Polly’s defaults need 100 calls in 30 s. Lower minimumThroughput, or use consecutiveFailures.
  • A breaker per request. Build the pipeline once per dependency and reuse it.
  • Counting results. fetch resolves with a 503 Response. Use shouldHandle: new PredicateBuilder<Response>().handleResult((r) => r.status >= 500).