Skip to content
Buy me a coffee

From opossum

Who this is for: projects that wrap functions in opossum’s CircuitBreaker and want retries, hedging, rate limiting, typed errors, shared breaker state across instances, or ESM and TypeScript types in the package.

opossum is one object that bundles a circuit breaker, a timeout, a concurrency cap (capacity), a cache and a fallback. Here each of those is a separate strategy in a pipeline, and you pick the order. A pipeline wraps a call, not a function: pipeline.execute(() => fn(a, b)), or pipeline.wrap(fn) to get a function with the same arguments, like breaker.fire(a, b).

opossum 10 @ylcnfrht/resilience
new CircuitBreaker(fn, options) + breaker.fire(a, b) builder.addCircuitBreaker(…).build() + pipeline.execute(() => fn(a, b))
fire with the action’s arguments const call = pipeline.wrap(fn); call(a, b)
timeout (default 10 s, on by default) addTimeout(ms) after the breaker (off unless you add it)
timeout: false leave out addTimeout
errorThresholdPercentage: 50 failureRatio: 0.5
resetTimeout (default 30 s) breakDuration (default 5 s)
rollingCountTimeout (default 10 s) samplingDuration (default 30 s)
rollingCountBuckets no option: the window’s resolution is internal
volumeThreshold (default 0) minimumThroughput (default 100)
allowWarmUp (warm-up = rollingCountTimeout) warmUpDuration: '10s' (any length)
errorFilter: (err) => true (don’t count) shouldHandle: ({ outcome }) => … returns false (see differences)
capacity addConcurrencyLimiter(n) (rejects at once, like opossum; a queue is optional)
abortController / autoRenewAbortController / getSignal() context.signal, a fresh one per call
breaker.fallback(fn) addFallback({ fallbackAction }) as the first (outermost) strategy
cache, cacheTTL, cacheGetKey, cacheTransport addCache({ ttl, key, store })
coalesce, coalesceTTL addCoalesce() or addCache({ ttl, coalesce: true })
breaker.healthCheck(fn, interval) extensions: [circuitHealthCheck({ check, interval })]
breaker.open() / breaker.close() manualControl.isolate() / manualControl.close()
breaker.disable() / enable() pipeline.disable() / enable() (bypasses every strategy)
breaker.shutdown() pipeline.dispose()
breaker.opened / closed / halfOpen / warmUp stateProvider.circuitState / stateProvider.isWarmingUp
breaker.toJSON() + { state } option stateProvider.snapshot() + initialState
breaker.stats createStats(), or stateProvider.metrics for the breaker window
name, group builder name, tags: { group: '…' }
opossum-prometheus instrumentPrometheus() from @ylcnfrht/resilience/prometheus
opossum-hystrix createHystrixStream() from @ylcnfrht/resilience/hystrix
opossum event @ylcnfrht/resilience
fire telemetry OnPipelineExecuting
success / failure telemetry OnStrategyExecuted (args.outcome.ok)
reject breaker onRejected
timeout timeout onTimeout
open / close / halfOpen onOpened / onClosed / onHalfOpened
fallback fallback onFallback
semaphoreLocked concurrency limiter onRejected
healthCheckFailed telemetry OnHealthCheckFailed
cacheHit / cacheMiss cache onCacheHit / onCacheMiss

Telemetry events are read with onTelemetry((e) => …). See Events.

opossum error.code Class code
EOPENBREAKER BrokenCircuitError CIRCUIT_OPEN (CIRCUIT_ISOLATED when held open)
ETIMEDOUT TimeoutRejectedError TIMEOUT
ESEMLOCKED BulkheadRejectedError BULKHEAD_FULL
import CircuitBreaker from 'opossum';
const breaker = new CircuitBreaker(getPrice, {
timeout: 3000,
errorThresholdPercentage: 50,
resetTimeout: 30000,
volumeThreshold: 10,
});
breaker.fallback(() => cachedPrice);
breaker.on('open', () => log.warn('prices circuit open'));
const price = await breaker.fire(productId);

Order matters: the fallback is first so it also catches the open circuit, and the timeout is inside the breaker so timeouts count as failures. That is the order opossum uses internally. Pass the signal on: when the timeout fires, it aborts, so the slow request stops instead of running in the background.

const breaker = new CircuitBreaker(sendEmail, { timeout: 5000 });
await breaker.fire(to, subject, body);

wrap() doesn’t pass the signal to your function. Inside the call, getResilienceSignal() returns it (on runtimes with AsyncLocalStorage, see Runtimes).

const breaker = new CircuitBreaker(getUser, {
capacity: 20,
cache: true,
cacheTTL: 60_000,
cacheGetKey: (id) => `user:${id}`,
});
breaker.healthCheck(() => db.ping(), 10_000);
const user = await breaker.fire(id);

The cache key comes from the execution (operationKey, or a key: (context) => … option), not from the function’s arguments. Concurrent misses for the same key share one call by default (coalesce: true).

const breaker = new CircuitBreaker(fn, { state: saved.state });
process.on('SIGTERM', () => save(breaker.toJSON()));

The snapshot includes the rolling window, so a restored breaker keeps its failure history. To share live state between instances instead, see Redis.

  • Threshold comparison. opossum opens when the error rate is above errorThresholdPercentage; ours opens when the ratio reaches failureRatio. At exactly 50% opossum stays closed and we open.
  • Filtered errors. An error for which opossum’s errorFilter returns true counts as a success and skips the fallback. An error our shouldHandle rejects is ignored: not a failure, not a success. Our default already ignores 4xx client errors (except 408/429), caller aborts and rejections from other strategies; opossum counts every rejection unless you filter it.
  • Half-open. opossum lets the next call through after resetTimeout and reopens on the first failure. We let halfOpenMaxCalls probes through (default 1), and a probe that runs longer than halfOpenMaxWait (30 s) counts as failed.
  • Fallback scope. opossum’s fallback runs for any failure of that breaker, gets the fire arguments, and keeps running while the circuit is open. Ours wraps whatever is after it, receives { outcome, context }, and by default skips caller aborts and 4xx client errors.
  • Cache key. opossum keys the cache by JSON.stringify(args). We have no access to the arguments: give each call an operationKey or a key function. Executions without a key bypass the cache.
  • Coalescing lifetime. opossum’s coalesce holds results for coalesceTTL (default: the timeout). addCoalesce() only shares calls that are in flight at the same time; use addCache({ ttl }) to keep results longer.
  • One signal per call. opossum’s abortController is shared by all calls of a breaker, which is why autoRenewAbortController exists. Here every execution and every attempt gets its own context.signal.
  • disable() bypasses every strategy of the pipeline, not only the breaker. To force the breaker alone, use manualControl.
  • No EventEmitter. Callbacks are options, and onTelemetry() returns an unsubscribe function, so there is no MaxListenersExceededWarning and no shutdown() needed to release listeners.
  • Retry with backoff and Retry-After, hedging, rate limiting, adaptive concurrency, lock, chaos, retry budget.
  • Typed errors with settle() and Outcome.match(); deadline propagation into nested calls.
  • Breaker state shared across instances through Redis, not only snapshots.
  • TypeScript types in the package, ESM and CommonJS, a FakeTimeProvider for tests, OpenTelemetry export.

Related: Circuit breaker, Cache, Fallback.