Skip to content
Buy me a coffee

Why @nodelibraries/resilience

If you know Polly, you know this API: ResiliencePipelineBuilder, addRetry, addCircuitBreaker, shouldHandle, PredicateBuilder, ResiliencePropertyKey, OnRetry and OnCircuitOpened events, Simmy-style chaos. The differences are JavaScript ones: durations are milliseconds or strings like '30s', and CancellationToken is an AbortSignal. See Migrating from Polly.

Hedging. When a call is slow, start a second one and take whichever answers first. It cuts tail latency (p99) on reads without adding a timeout.

const
const search: ResiliencePipeline<any, never>
search
= 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>.addHedging(options?: HedgingOptions<any>): ResiliencePipelineBuilder<any, never, never>
addHedging
({
HedgingOptions<any>.delay?: Duration | undefined

ms to wait before starting another attempt. Default 2000. > 0 latency mode, 0 parallel mode (all at once), < 0 fallback mode (next attempt only after a failure).

delay
: '150ms',
HedgingOptions<any>.maxHedgedAttempts?: number | undefined

Hedged attempts in addition to the primary. Default 1.

maxHedgedAttempts
: 1 }) // second request after 150 ms; the loser is aborted
.
ResiliencePipelineBuilder<any, never, never>.build(): ResiliencePipeline<any, never>
build
();

Adaptive concurrency. A bulkhead whose limit tunes itself from observed latency, with the gradient2, vegas and AIMD algorithms known from Netflix’s concurrency-limits. You stop guessing “how many parallel calls can this database take”.

const
const db: ResiliencePipeline<any, BulkheadRejectedError>
db
= 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>.addAdaptiveConcurrencyLimiter(options?: AdaptiveConcurrencyLimiterOptions<any>): Next<any, never, never, BulkheadRejectedError>

Concurrency limit that tunes itself from observed latency and failures (gradient2 / vegas / aimd), so you don't have to guess the right number: it grows while the dependency keeps up and backs off when it queues.

addAdaptiveConcurrencyLimiter
({
AdaptiveConcurrencyLimiterOptions<any>.algorithm?: "vegas" | "gradient2" | "aimd" | LimitAlgorithm | undefined

Default 'gradient2'. Pass an object for custom tuning (vegas({ alpha: 2 })) or your own algorithm.

algorithm
: 'gradient2',
AdaptiveConcurrencyLimiterOptions<any>.minLimit?: number | undefined

Default 1.

minLimit
: 5,
AdaptiveConcurrencyLimiterOptions<any>.maxLimit?: number | undefined

Default 1000.

maxLimit
: 200 })
.
ResiliencePipelineBuilder<any, BulkheadRejectedError, never>.build(): ResiliencePipeline<any, BulkheadRejectedError>
build
();

Also built in: a retry budget against retry storms, request batching, distributed locks with fencing tokens, and Redis-backed limiters and breaker state.

A pipeline executed inside another one inherits its signal through AsyncLocalStorage. A 3-second budget on an incoming request cancels every downstream call made under it, even ones that never received the signal. See Context and signals.

pipeline.settle() returns an outcome whose error type is computed from the strategies you added. Outcome.match() then requires a handler per error code. Add a circuit breaker, forget to handle CIRCUIT_OPEN, and the build fails. See Typed errors.

  • Zero runtime dependencies. Integrations (NestJS, OpenTelemetry, axios…) are optional peers in subpaths.
  • Every runtime. Node.js ≥ 20.16, Bun, Deno, Cloudflare Workers, browsers. No static node: imports.
  • Tree-shakeable. The retry() one-liner is 4.1 KB min+gzip; retry + breaker + timeout is 7.1 KB. See Bundle size.
  • Testable. FakeTimeProvider runs 30 seconds of backoff instantly.

Retries and circuit breakers ignore 4xx client errors, caller aborts and rejections from other strategies. Retrying a 404 can’t help, and a breaker that counts its own “circuit open” rejections never closes. An open circuit is never retried. Calls marked idempotent: false are never repeated once they may have reached the dependency. See What counts as a failure.

  • You need one retry loop and nothing else. p-retry is smaller. The retry() one-liner is close, but a dedicated library is still lighter.
  • You use every strategy through ResiliencePipelineBuilder in a browser bundle. The full builder is 15.1 KB min+gzip. Switch to PipelineBuilder to pay only for what you use.
  • You want durable workflows (retries that survive a process restart, sagas). That is a job for a workflow engine or a queue, not an in-process pipeline.