Skip to content

resilient-pipeline

Zero dependencies · Node, Bun, Deno, Workers, browsers

Failures are inevitable.
Outages are optional.

Retry, circuit breaker, timeout, hedging, rate limiting and typed errors, composed into one pipeline. A Polly v8 API for TypeScript, from 4 KB.

payments.ts
import { ResiliencePipelineBuilder } from 'resilient-pipeline';
const payments = new ResiliencePipelineBuilder({ name: 'payments' })
.addFallback({ fallbackAction: () => queueForLater() })
.addRetry({ maxRetryAttempts: 3, backoffType: 'exponential', useJitter: 'full' })
.addCircuitBreaker({ failureRatio: 0.5, minimumThroughput: 20 })
.addTimeout('2s')
.build();
const receipt = await payments.execute(({ signal }) => charge(order, { signal }));
0dependencies
4 KBretry, min+gzip
14strategies
230+tests
ESM + CJStyped, tree-shakeable

What it does

Every failure mode, one pipeline.

Failures

Retry what is worth retrying. Stop hammering what is down.

Exponential backoff with jitter and Retry-After, a circuit breaker that opens before the outage spreads, and fallbacks that answer anyway. 4xx errors are never retried.

Read more
.addRetry({ backoffType: 'exponential', useJitter: 'full' })
.addCircuitBreaker({ consecutiveFailures: 5, breakDuration: '30s' })
.addFallback({ fallbackValue: cached })
Overload

Limits that tune themselves.

Bulkheads with priority queues, token buckets and sliding windows per user, and an adaptive concurrency limit that finds the right number from latency.

Read more
.addAdaptiveConcurrencyLimiter({ algorithm: 'gradient2' })
.addPartitionedRateLimiter({ partitionKey: (ctx) => tenant(ctx), … })
Latency

Cut the slow tail.

Hedging races a second attempt against a slow one. Timeouts abort the work, not just the wait, and deadlines flow into every nested call.

Read more
.addHedging({ delay: '200ms', maxHedgedAttempts: 2 })
.addTimeout('2s') // nested pipelines inherit the deadline
Typed errors

The compiler knows every way it can fail.

settle() returns a typed outcome. Outcome.match makes you handle each error code, and forgetting one is a compile error.

Read more
Outcome.match(await pipeline.settle(load), {
ok: (user) => user,
TIMEOUT: () => cached,
CIRCUIT_OPEN: () => cached,
UNEXPECTED: (e) => { throw e },
})
Distributed

One decision across every instance.

Share circuit state, rate limits, locks and caches through Redis. Atomic Lua scripts, fencing tokens, Cluster-safe keys.

Read more
extensions: [sharedCircuitState({ store: new RedisCircuitStateStore({ client }), key: 'payments' })]
Testing

Test a 30-second outage in milliseconds.

FakeTimeProvider drives every retry delay, timeout and open circuit. No sleeps, no flaky tests.

Read more
const time = new FakeTimeProvider();
const done = assert.rejects(pipeline.execute(neverResolves));
await time.runAll(); // 34 virtual seconds, instantly

Only here

What the others don't ship.

  • HedgingRace a second attempt against the slow tail.
  • Adaptive concurrencygradient2, vegas and aimd limits, no magic numbers.
  • Deadline propagationAn outer timeout cancels every nested call.
  • Typed errors on a Polly APIsettle() and exhaustive Outcome.match.
  • Retry budgetsRetries capped at a share of traffic, so no retry storms.
  • Redis-shared stateBreakers, limits, locks and caches across instances.
Full comparison
resilient-pipelinecockatielopossumtenace
Retry · breaker · timeout · fallback✓✓✓✓
Hedging✓–––
Adaptive concurrency✓–––
Typed error channel✓––✓
Shared state across instances✓––✓
Fake clock for tests✓––✓
Runs in browsers & Workers✓✓✓✓

The toolbox

Everything a call to a flaky dependency needs.

Failure

  • addRetryBackoff, jitter, budgets
  • addCircuitBreakerStop calling what is down
  • addTimeoutAbort, not just wait
  • addFallbackAnswer anyway
  • addHedgingRace the slow tail

Load

  • addConcurrencyLimiterBulkhead with priorities
  • addAdaptiveConcurrencyLimiterSelf-tuning limit
  • addRateLimiterToken bucket
  • addPartitionedRateLimiterPer user, per tenant
  • addCache · addCoalesceCache and single-flight

Types & flow

  • settle()Typed outcome, never throws
  • Outcome.match()Exhaustive by error code
  • executeStream()Commit after the first chunk
  • addLockLeases with fencing tokens
  • createBatcherDataLoader-style batching

Run & observe

  • /fetch · /axiosHTTP clients, per host
  • /http · /fastify · /koaInbound deadlines, shedding
  • /otel · /prometheusSpans and metrics
  • FakeTimeProviderTest without waiting
  • setGlobalChaosGame days on demand

In code

Real problems, a few lines each.

import { createResilientFetch } from 'resilient-pipeline/fetch';
// Per-host breakers, retries that honour Retry-After, idempotency-aware POSTs.
const http = createResilientFetch({ name: 'github', retryOn: [429, 503] });
const res = await http('https://api.github.com/repos/nodejs/node', {
resilience: { timeout: '3s' },
});

Get started

Wrap one call. Grow from there.

Start with a retry and a timeout. Add a breaker when the first incident teaches you why.