npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

breakwater

v1.1.2

Published

Resilience toolkit for Node.js — retry, circuit breaker, timeout, and policy composition with first-class observability.

Readme

breakwater

Resilience toolkit for Node.js — retry, circuit breaker, timeout, fallback, and policy composition with first-class observability.

CI npm version license

When waves of failure hit, the breakwater keeps your service standing. It brings the design of resilience4j (Java) and Polly (.NET) to Node.js: composable resilience policies with explicit ordering, typed events, and metrics — no third-party plugins required.

import { resilience, exponential } from 'breakwater'

const payments = resilience({
  retry: { attempts: 3, backoff: exponential({ initial: 200 }) },
  circuitBreaker: { name: 'payments-api', failureThreshold: 0.5 },
  timeout: 2_000,
  fallback: () => ({ status: 'pending', queued: true })
})

const charge = await payments.execute(({ signal }) => api.post('/charge', body, { signal }))

Table of contents

Why another resilience library?

| | breakwater | opossum | cockatiel | |---|---|---|---| | Circuit breaker | ✅ | ✅ | ✅ | | Retry with backoff strategies | ✅ | ➖ rudimentary | ✅ | | Timeout / fallback | ✅ | ➖ fallback only | ✅ | | Bulkhead | ✅ | ❌ | ✅ | | Rate limiter (token bucket / sliding window) | ✅ | ❌ | ❌ | | Stale-while-open cache (last good response) | ✅ staleCache | ❌ | ❌ | | Policy composition with explicit ordering | ✅ first-class | ❌ | ✅ | | Named policy registry (central config) | ✅ | ❌ | ❌ | | Typed events + pluggable metrics collector | ✅ native | ➖ via plugin | ❌ | | Prometheus / OpenTelemetry adapters | ✅ /prometheus · /otel | ➖ via plugin | ❌ | | Distributed circuit breaker state (Redis) | ✅ /redis | ❌ | ❌ | | TypeScript | ✅ native | ➖ via @types | ✅ native | | Runtime dependencies | 0 | 0 | 0 |

Design principles:

  • TypeScript first — strict types, no @types package
  • Zero dependencies in the core — integrations ship as optional entry points (Prometheus, OpenTelemetry and Redis)
  • Declarative composition — policies combine into pipelines with explicit, documented ordering
  • Native observability — typed events and a pluggable MetricsCollector in the core

Install

npm install breakwater

Works with both module systems:

import { retry, timeout, circuitBreaker } from 'breakwater' // ESM
const { retry, timeout, circuitBreaker } = require('breakwater') // CJS

Quick start

Protect a flaky HTTP call in three lines:

import { retry } from 'breakwater'

const policy = retry({ attempts: 3 })
const user = await policy.execute(() => fetchUser(id))

Add a time budget and a circuit breaker, composed in an explicit order:

import { compose, retry, circuitBreaker, timeout } from 'breakwater'

const policy = compose(
  retry({ attempts: 3 }),                      // outermost
  circuitBreaker({ name: 'user-service' }),
  timeout(2_000)                               // innermost, hugs the function
)

const user = await policy.execute(({ signal }) => fetchUser(id, { signal }))

Or let resilience() pick the battle-tested default order for you:

import { resilience } from 'breakwater'

const policy = resilience({
  retry: { attempts: 3 },
  circuitBreaker: { name: 'user-service' },
  timeout: 2_000,
  fallback: cachedUser
})

Core concepts

Every policy speaks the same contract

A policy — and the result of composing policies — is an object with:

policy.execute(fn, options?)  // run fn under the policy's protection
policy.wrap(fn)               // decorate: same signature in, protected function out
policy.invoke(fn, ctx)        // composition primitive (used by compose())

Two everyday shapes:

// 1. execute — fn receives the execution context (with the combined signal)
const data = await policy.execute(({ signal }) => fetch(url, { signal }))

// 2. wrap — decorate once, call everywhere
const safeFetchUser = policy.wrap(fetchUser)
const user = await safeFetchUser(id)

wrap keeps the function signature untouched, so the wrapped function does not receive the context. Use execute when you need the inner signal. Also note this is not forwarded — bind methods first: policy.wrap(svc.method.bind(svc)).

The execution context

One context travels through the whole pipeline:

interface ExecutionContext {
  signal: AbortSignal    // external cancellation + timeouts, combined into ONE signal
  attempt: number        // 0 on the first execution; incremented by retry
  correlationId: string  // generated if not provided; present in every event payload
  metadata: Record<string, unknown>  // yours, crosses every policy
}

Your function only ever needs to observe one AbortSignal — the policies combine external cancellation, timeouts and retry cancellation into it:

await policy.execute(
  ({ signal }) => fetch(url, { signal }),
  { signal: request.signal, correlationId: request.id }
)

Cancellation is not failure

Aborting via AbortSignal is treated as cancellation everywhere: retry does not retry it, the circuit breaker does not count it, and fallback does not replace it. The abort reason propagates to the caller untouched.

The policies

| Policy | One-liner | Docs | |---|---|---| | timeout(ms, options?) | Bound the time of each execution, cooperatively or aggressively | docs/timeout.md | | retry(options?) | Retry transient failures with configurable backoff and a total deadline | docs/retry.md | | circuitBreaker(options?) | Fail fast while a dependency is down; probe and recover automatically | docs/circuit-breaker.md | | bulkhead(options?) | Bound concurrent executions, with an optional FIFO wait queue | docs/bulkhead.md | | rateLimit(options) | Cap the execution rate — token bucket or exact sliding window | docs/rate-limit.md | | fallback(handler, options?) | Replace a failure with a value, a function result, or a chain of them | docs/fallback.md | | staleCache(options?) | Serve the last good response while the circuit is open | docs/stale-cache.md | | compose(...policies) | Combine policies with explicit ordering; compositions compose again | docs/composition.md | | resilience(options) | The batteries-included pipeline with a sane default order | docs/composition.md |

Composing policies

compose(a, b, c) runs exactly like the nested calls a(b(c(fn))) — the first policy is the outermost. Order changes behavior: retry outside the circuit breaker behaves very differently from retry inside it. This is the part most libraries leave undocumented; we document it with diagrams in docs/composition.md.

The default order used by resilience():

fallback( retry( rateLimit( bulkhead( circuitBreaker( timeout( fn ) ) ) ) ) )

Every attempt flows through the breaker (feeding its stats individually), and once the circuit opens, retry sees CircuitOpenError — which is not retryable — and gives up immediately instead of hammering an open circuit.

Named policies

Define your resilience configuration once, at startup; ask for policies by name everywhere else — same name, same instance, genuinely shared state:

import { policies } from 'breakwater'

// config/resilience.ts
policies.define('payments-api', {
  retry: { attempts: 3 },
  circuitBreaker: { failureThreshold: 0.5 },
  timeout: 2_000
})

// anywhere else
await policies.get('payments-api').execute(({ signal }) => api.post('/charge', body, { signal }))

Typos fail fast (get throws listing the defined names), duplicates throw, and the registry name flows into metrics automatically. See docs/named-policies.md.

Observability

Every policy emits typed events — no plugin required:

const breaker = circuitBreaker({ name: 'payments-api' })

breaker
  .on('stateChange', ({ from, to, stats }) => log.warn({ from, to, stats }, 'circuit state changed'))
  .on('reject', ({ correlationId }) => log.debug({ correlationId }, 'request rejected fast'))

breaker.stats()
// { state, successes, failures, totalCalls, failureRate, latency, lastError, openedAt, nextAttemptAt }
// latency: { count, min, max, mean, p50, p95, p99 } over the same window

For metrics pipelines, implement the MetricsCollector interface once and plug it into resilience() — it wires every policy for you:

const policy = resilience({
  retry: { attempts: 3 },
  circuitBreaker: { name: 'payments-api' },
  timeout: 2_000,
  metrics: myCollector // onExecution, onRetry, onTimeout, onStateChange, onFallback, onReject
})

Building with compose() instead? attachMetrics(pipeline, collector) wires a whole composition in one call, and metricsPolicy(collector) measures the pipeline as a regular outermost policy. Compositions also expose an aggregated stats() of their inner policies.

Don't want to write a collector? Two ready-made adapters ship as optional entry points. breakwater/prometheus emits prom-client metrics — executions, durations, rejections and circuit states — plus a Grafana dashboard to import:

import { prometheusCollector } from 'breakwater/prometheus' // prom-client is a peer dependency

const policy = resilience({ name: 'payments-api', metrics: prometheusCollector() })

breakwater/otel emits the same signals as OpenTelemetry metrics, and adds spanPolicy() — a composable policy that wraps each execution in an active span, so instrumented HTTP clients and database drivers nest under it in the trace:

import { otelCollector, spanPolicy } from 'breakwater/otel' // @opentelemetry/api is a peer dependency

const policy = resilience({ name: 'payments-api', metrics: otelCollector() })

See docs/prometheus.md and docs/otel.md.

Errors

Every error breakwater throws extends BreakwaterError and carries a stable code — branch on the code or the type guards, never on messages:

| Error | code | Thrown by | |---|---|---| | TimeoutError | TIMEOUT | timeout | | RetryExhaustedError | RETRY_EXHAUSTED | retry (last error in cause) | | CircuitOpenError | CIRCUIT_OPEN | circuit breaker (carries stats) | | IsolatedError | CIRCUIT_ISOLATED | circuit breaker (manual isolation) | | BulkheadRejectedError | BULKHEAD_REJECTED | bulkhead (carries stats; stays retryable) | | RateLimitedError | RATE_LIMITED | rate limit (carries stats and retryAfterMs; stays retryable) | | FallbackFailedError | FALLBACK_FAILED | fallback (operation error in originalError) |

import { isCircuitOpenError, isTimeoutError } from 'breakwater'

try {
  await policy.execute(chargeCard)
} catch (error) {
  if (isCircuitOpenError(error)) return res.status(503).json({ retryAfter: error.stats.nextAttemptAt })
  if (isTimeoutError(error)) return res.status(504).end()
  throw error
}

See docs/errors.md.

Documentation

Requirements

  • Node.js >= 22

License

MIT