@arki/resilience
archivefor @arki/[email protected] · Tier 2: standalone libraries
Resilience primitives for ARKI — retry, circuit breaker, timeout, bulkhead, and curated presets.
Version 0.1.0 archive
Install
bun add @arki/resilienceOther package managers
npm install @arki/resilience
pnpm add @arki/resilience
yarn add @arki/resilienceRelease health for @arki/[email protected]
Doc examples
No example report was recorded for this version.
Open gaps
6 release checks fail
- check
file-llms-txt - check
file-agents-md - check
file-agent-guide - check
readme-h2-order - check
pkg-engines - check
files-ship-dist-docs
Used by demos
No demo uses this package yet.
Runtimes
No engines field, so no runtime requirement is declared.
Benchmarks
No benchmarks are published for this version.
README
Resilience primitives for ARKI — retry, circuit breaker, timeout, bulkhead, and curated presets. Built on the cockatiel engine with ergonomic factories, ready-made policies for common workloads, and its own public types and errors, so a new cockatiel release never changes this package's API.
Installation
npm install @arki/resilience
# or
bun add @arki/resilience
# or
pnpm add @arki/resilienceUsage
Pre-configured presets
import { externalApiPolicy, withResilience } from '@arki/resilience';
const data = await withResilience(
() => fetch('https://api.example.com/billing').then(r => r.json()),
externalApiPolicy,
);Three curated presets ship out of the box:
externalApiPolicy— retry + timeout tuned for third-party HTTP calls.databasePolicy— short timeout + retry for transient connection errors.backgroundJobPolicy— longer timeout + aggressive retry for long-running work.
Building policies
import { createResiliencePolicy, withResilience } from '@arki/resilience';
const policy = createResiliencePolicy({
retry: { maxAttempts: 3, initialDelay: 200 },
timeout: { timeoutMs: 5_000 },
circuitBreaker: { halfOpenAfter: 30_000, consecutiveFailures: 5 },
});
const result = await withResilience(({ signal }) => doWork(signal), policy);Every policy has the same shape, the Policy type:
import type { Policy, PolicyContext } from '@arki/resilience';
const policy: Policy = createResiliencePolicy();
await policy.execute(({ signal, attempt }: PolicyContext) => fetchPage(signal), parentSignal);signal aborts when the caller's signal aborts, when a timeout policy reaches its deadline, and after a timed call returns. attempt is 0 for the first call and 1 for the first retry; it is always 0 under a policy without retry.
Quick wrappers
For one-off calls without a long-lived policy:
import { withRetry, withTimeout } from '@arki/resilience';
await withRetry(() => sendEmail('[email protected]'), { maxAttempts: 3 });
await withTimeout(() => slowOperation(), { timeoutMs: 1_000 });A circuit breaker must remember past failures, so build it once and reuse it:
const breaker = createCircuitBreakerPolicy({ consecutiveFailures: 2 });
await withResilience(() => callDownstream(), breaker); // opens after 2 failures in a rowErrors
Errors thrown by your function reach you unchanged. Errors raised by the policies themselves all extend ResilienceError:
CircuitOpenError— the breaker is open; the function was not called.TimeoutError— a timeout policy reached its deadline.CancelledError— the caller's own signal aborted;causeis the signal's reason.BulkheadRejectedError— every slot and queue place of a bulkhead was taken.
import { isBrokenCircuitError, isResilienceError } from '@arki/resilience';
try {
await withResilience(() => callDownstream(), policy);
} catch (error) {
if (isBrokenCircuitError(error)) return cachedAnswer;
if (isResilienceError(error)) throw new ServiceUnavailable(error);
throw error;
}Observability
import { attachPolicyLogger } from '@arki/resilience';
const detach = attachPolicyLogger(policy, logger, 'billing');logger needs info, warn and error methods. The helper subscribes to the events a policy exposes: every policy has onSuccess and onFailure; a retry policy adds onRetry and onGiveUp; a circuit breaker adds onBreak, onHalfOpen and onReset; a timeout policy adds onTimeout; a bulkhead adds onReject. Each event method takes a listener and returns a subscription with dispose(). A composed policy (wrap, createResiliencePolicy, the presets) exposes only onSuccess and onFailure.
API
createResiliencePolicy(options)— compose retry + timeout + circuit breaker + bulkhead.createRetryPolicy,createCircuitBreakerPolicy,createTimeoutPolicy,createBulkheadPolicy— individual primitives.externalApiPolicy,databasePolicy,backgroundJobPolicy— pre-tuned presets.withResilience,withRetry,withTimeout— execution helpers.withCircuitBreaker— deprecated. It builds a new breaker on every call, so it never opens. UsecreateCircuitBreakerPolicy()once andwithResilience()per call.attachPolicyLogger(policy, logger)— observability hooks.wrap(...policies)— compose policies from the outside in;noop— a policy that only runs the function.ResilienceError,CircuitOpenError,TimeoutError,CancelledError,BulkheadRejectedError,RetriesExhaustedError— typed errors with helpers (isResilienceError,isBrokenCircuitError,isBulkheadRejectedError).- Types:
Policy,PolicyContext,PolicyCallback,RetryPolicy,CircuitBreakerPolicy,TimeoutPolicy,BulkheadPolicy,PolicyEvent,Subscriptionand the event payloadsSuccessEvent,FailureEvent,FailureReason,RetryEvent,BreakEvent.
When does the overhead matter?
Almost never. A policy adds a few microseconds to a call: the benchmarks below measure that on a call that returns at once, and on a call that waits 1 ms. The calls these policies guard, to a network service, an AI provider or a database, take milliseconds to seconds, so the policy costs well under one percent of the call it protects. The one case to think about is a hot loop over an in-memory function, where the policy can cost more than the work. There, wrap the loop, not each iteration.
Documentation
@arki/resilience is framework-agnostic and works on its own. When you
compose it with the @arki/dot
application framework, see packages/dot/docs/ for plugin authoring,
lifecycle, and diagnostics.
Benchmarks
The cost of wrapping a call that succeeds, with @arki/resilience, against cockatiel (which it wraps), p-retry and opossum, and a bare await fn() for scale. These are the three libraries most often reached for to retry, break a circuit or time out in Node.
Node v22.13.1 on Apple M2 Max, 2026-10-13. Median time per operation, lower is better.
Retry wrapper
| Library | Time / op | vs fastest |
|---|---|---|
| @arki/resilience createRetryPolicy() + withResilience() | 1.21 µs | 9.66× |
| @arki/resilience withRetry() (builds the policy each call) | 875.0 ns | 7.00× |
| cockatiel retry().execute() (used directly) | 375.0 ns | 3.00× |
| p-retry pRetry(fn, { retries: 3 }) | 667.0 ns | 5.34× |
| plain await fn() | 125.0 ns | fastest |
Circuit breaker
| Library | Time / op | vs fastest |
|---|---|---|
| @arki/resilience createCircuitBreakerPolicy() + withResilience() | 334.0 ns | 4.02× |
| cockatiel circuitBreaker().execute() (used directly) | 292.0 ns | 3.52× |
| opossum new CircuitBreaker(fn, { timeout: false }).fire() | 875.0 ns | 10.54× |
| plain await fn() | 83.0 ns | fastest |
Timeout wrapper
| Library | Time / op | vs fastest |
|---|---|---|
| @arki/resilience createTimeoutPolicy() + withResilience() | 6.67 µs | 80.33× |
| @arki/resilience withTimeout() (builds the policy each call) | 6.42 µs | 77.31× |
| cockatiel timeout().execute() (used directly, aggressive) | 4.25 µs | 51.20× |
| opossum new CircuitBreaker(fn, { timeout }).fire() (breaker included) | 1.17 µs | 14.05× |
| Promise.race + setTimeout (hand-rolled) | 958.0 ns | 11.54× |
| plain await fn() | 83.0 ns | fastest |
Combined: retry + circuit breaker + timeout
The preset an application would use for an external API call. The p-retry + opossum row composes the two libraries by hand to cover all three jobs.
| Library | Time / op | vs fastest |
|---|---|---|
| @arki/resilience externalApiPolicy + withResilience() | 5.04 µs | 60.75× |
| cockatiel wrap(retry, breaker, timeout) (used directly) | 4.54 µs | 54.72× |
| p-retry around an opossum breaker with timeout | 1.50 µs | 18.07× |
| plain await fn() | 83.0 ns | fastest |
Read the rows with the contracts in mind:
- Every call succeeds on the first try. The numbers show the overhead on a healthy call, not behaviour under failure: how fast a breaker opens, how backoff spaces retries, and what happens to in-flight calls are not measured here.
- Policies are built once and reused, which is how
@arki/resilienceexpects them to be used. ThewithRetry()andwithTimeout()rows build a new policy on every call (that is how those helpers work), so they include construction. @arki/resiliencedefaults match cockatiel here (3 attempts, exponential backoff, a breaker that opens on 5 consecutive failures, an aggressive timeout), and the cockatiel rows use the same values. opossum counts failures in a rolling window and keeps statistics, p-retry only retries, and opossum has no separate timeout policy, so those rows do different amounts of work.- The bare
await fn()row is a floor, not a rival: it is what a call costs with no protection.
Reproduce with bun run bench in the package directory; regenerate this section with bun run bench --write-readme.
License
MIT — see LICENSE.
Public imports
This version has no API index.