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

@async-kit/retryx

v0.2.0

Published

Smart async retry system with exponential backoff, jitter, and circuit breaker for JavaScript/TypeScript

Readme

npm TypeScript License: MIT Bundle size Node Browser

Smart async retry with exponential backoff, jitter strategies, circuit breaker, and AbortSignal support.

Make any async operation resilient in one line.


Install

npm install @async-kit/retryx

Quick Start

import { retry, createRetry, withRetry, CircuitBreaker } from '@async-kit/retryx';

// One-shot retry
const data = await retry(() => fetch('/api').then(r => r.json()), {
  maxAttempts: 5,
  jitter: 'full',
});

// Reusable retry function
const resilient = createRetry({ maxAttempts: 3, initialDelay: 200 });
await resilient(() => callApi());

// Wrap any async function
const safeFetch = withRetry(fetch, { maxAttempts: 3 });
const resp = await safeFetch('/api/users', { method: 'GET' });

// Circuit breaker
const cb = new CircuitBreaker({ failureThreshold: 5, successThreshold: 2, openDurationMs: 10_000 });
const result = await cb.run(() => callExternalService());

API

retry(task, options?)

| Option | Type | Default | Description | |---|---|---|---| | maxAttempts | number | 3 | Total attempts including the first | | initialDelay | number | 200 | Base delay in ms before the first retry | | maxDelay | number | 30_000 | Maximum delay cap in ms | | factor | number | 2 | Exponential backoff multiplier | | jitter | JitterStrategy | 'equal' | Randomization strategy (see below) | | retryIf | (err, ctx) => bool | () => true | Return false to stop retrying; may be async | | onRetry | (n, err, ms, ctx) => void | — | Hook called before each retry delay | | signal | AbortSignal | — | Cancels pending retry delays | | timeoutMs | number | — | Per-attempt timeout; throws RetryxTimeoutError |

Jitter Strategies

| Strategy | Formula | Best For | |---|---|---| | 'equal' (default) | cap/2 + random(0, cap/2) | Preserves mean delay | | 'full' | random(0, cap) | AWS-recommended; highest spread | | 'decorrelated' | min(maxDelay, random(base, max(base, prev×3))) | Aggressive thundering-herd prevention | | 'none' | cap | Deterministic testing |

'decorrelated' note: The formula guards against a negative random range when prevDelay is very small by using max(initialDelay, prevDelay × 3) as the upper bound — delays are always ≥ initialDelay.

createRetry(defaults)

Creates a reusable retry function. Per-call overrides are merged with defaults.

const resilient = createRetry({ maxAttempts: 5, jitter: 'full' });
await resilient(() => fetchData(), { maxAttempts: 3 }); // override for this call

withRetry(fn, options)

Wraps an existing async function so every invocation is automatically retried.

const safeFetch = withRetry(fetch, { maxAttempts: 3 });
const resp = await safeFetch('/api/users', { method: 'GET' }); // retried transparently

RetryContext

Passed to retryIf and onRetry:

interface RetryContext {
  attemptNumber: number;  // 1-based attempt that just failed
  totalAttempts: number;
  elapsedMs: number;      // wall-clock ms since first attempt
  errors: unknown[];      // all errors so far, in order
}

Circuit Breaker

Prevents cascading failures by fast-failing calls when a service is degraded.

const cb = new CircuitBreaker({
  failureThreshold: 5,    // open after 5 consecutive failures
  successThreshold: 2,    // close after 2 successes in HALF_OPEN
  openDurationMs: 10_000, // stay open 10 s before probing
  volumeThreshold: 10,    // require ≥ 10 calls before tripping
  onStateChange: (from, to) => console.log(`${from} → ${to}`),
});

States

CLOSED ──(failures >= threshold)──► OPEN ──(after openDurationMs)──► HALF_OPEN
   ▲                                                                      │
   └──────────(successes >= successThreshold)────────────────────────────┘

| Method | Description | |---|---| | .run(task) | Execute; throws CircuitOpenError when OPEN | | .reset() | Force to CLOSED state | | .stats() | Returns { failures, successes, calls, state } | | .currentState | Current CircuitState |

Error Types

| Class | When | |---|---| | RetryxError | All attempts exhausted — has .attempts, .lastError, .allErrors | | RetryxTimeoutError | Per-attempt timeoutMs exceeded — has .attempt, .timeoutMs | | CircuitOpenError | Call blocked by open circuit — has .retryAfterMs |

Examples

Retry only transient HTTP errors

import { retry } from '@async-kit/retryx';

const data = await retry(
  () => fetch('/api/orders').then(async r => {
    if (!r.ok) throw Object.assign(new Error(r.statusText), { status: r.status });
    return r.json();
  }),
  {
    maxAttempts: 5,
    jitter: 'full',
    retryIf: (err: any) => err.status == null || err.status >= 500,
    onRetry: (attempt, err: any, delayMs) => {
      console.warn(`[attempt ${attempt}] ${err.message} — retrying in ${delayMs}ms`);
    },
  }
);

App-wide resilience factory

import { createRetry } from '@async-kit/retryx';

// Define once, use everywhere
export const resilient = createRetry({
  maxAttempts: 4,
  initialDelay: 300,
  maxDelay: 15_000,
  jitter: 'equal',
});

// In your service layer
export const ordersApi = {
  create: (payload: OrderPayload) =>
    resilient(() => httpClient.post('/orders', payload)),
  get: (id: string) =>
    resilient(() => httpClient.get(`/orders/${id}`)),
};

withRetry — wrap third-party SDKs

import { withRetry } from '@async-kit/retryx';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';

const s3 = new S3Client({});

// Wrap the entire send method — every call is retried automatically
const resilientSend = withRetry(
  s3.send.bind(s3),
  { maxAttempts: 4, jitter: 'decorrelated' }
);

const response = await resilientSend(
  new GetObjectCommand({ Bucket: 'my-bucket', Key: 'data.json' })
);

Per-attempt timeout to bound total latency

import { retry, RetryxTimeoutError } from '@async-kit/retryx';

const result = await retry(
  () => slowExternalService.query(params),
  {
    maxAttempts: 3,
    timeoutMs: 2_000,    // each attempt must finish within 2 s
    initialDelay: 500,
  }
).catch((err) => {
  if (err instanceof RetryxTimeoutError)
    console.error(`Attempt ${err.attempt} timed out after ${err.timeoutMs}ms`);
  throw err;
});

Cancellable polling with AbortSignal

import { retry } from '@async-kit/retryx';

const controller = new AbortController();

// Cancel from the UI
document.getElementById('cancel')!.onclick = () => controller.abort();

const result = await retry(
  () => pollJobStatus(jobId).then(s => {
    if (s.status !== 'done') throw new Error('not ready');
    return s;
  }),
  {
    maxAttempts: 120,
    initialDelay: 1_000,
    maxDelay: 10_000,
    signal: controller.signal,
  }
);

Circuit Breaker — protect a downstream service

import { CircuitBreaker, CircuitOpenError } from '@async-kit/retryx';

const inventoryCb = new CircuitBreaker({
  failureThreshold: 5,
  successThreshold: 2,
  openDurationMs: 30_000,
  onStateChange: (from, to) => {
    logger.warn(`inventory circuit: ${from} → ${to}`);
    metrics.increment(`circuit.inventory.${to.toLowerCase()}`);
  },
});

async function getInventory(sku: string) {
  try {
    return await inventoryCb.run(() => inventoryService.get(sku));
  } catch (err) {
    if (err instanceof CircuitOpenError) {
      // Serve from cache while circuit is open
      return cache.get(`inventory:${sku}`) ?? { qty: 0 };
    }
    throw err;
  }
}

Circuit Breaker + retry together

import { retry, CircuitBreaker, CircuitOpenError } from '@async-kit/retryx';

const cb = new CircuitBreaker({ failureThreshold: 3, successThreshold: 1, openDurationMs: 10_000 });

const data = await retry(
  () => cb.run(() => externalService.fetch(id)),
  {
    maxAttempts: 5,
    retryIf: (err) => !(err instanceof CircuitOpenError), // don't retry open-circuit errors
    onRetry: (n, err) => console.log(`retry ${n}: ${err}`),
  }
);

Inspect all errors across attempts

import { retry, RetryxError } from '@async-kit/retryx';

try {
  await retry(() => unstableService.call(), { maxAttempts: 3 });
} catch (err) {
  if (err instanceof RetryxError) {
    console.error(`Failed after ${err.attempts} attempts`);
    err.allErrors.forEach((e, i) =>
      console.error(`  Attempt ${i + 1}:`, e)
    );
  }
}

Types

import type {
  JitterStrategy,
  RetryContext,
  RetryxOptions,
  CircuitState,
  CircuitBreakerOptions,
  CircuitBreakerStats,
} from '@async-kit/retryx';

Compatibility

| Environment | Support | Notes | |---|---|---| | Node.js | ≥ 18 | Recommended ≥ 24 for best performance | | Deno | ✅ | Via npm specifier (npm:@async-kit/retryx) | | Bun | ✅ | Full support | | Chrome | ≥ 80 | ESM via bundler or native import | | Firefox | ≥ 75 | ESM via bundler or native import | | Safari | ≥ 13.1 | ESM via bundler or native import | | Edge | ≥ 80 | ESM via bundler or native import | | React Native | ✅ | Via Metro bundler | | Cloudflare Workers | ✅ | ESM, AbortSignal natively supported | | Vercel Edge Runtime | ✅ | ESM, no process / fs dependencies |

No Node.js built-ins are used. The package relies only on standard JavaScript (Promise, setTimeout, clearTimeout, AbortSignal, DOMException) — all available in any modern runtime.

AbortSignal / DOMException are part of the Web Platform API. In Node.js they are globals since v15. In older environments (Node 14 or old browsers) you may need to polyfill AbortController — e.g. abortcontroller-polyfill.

License

MIT © async-kit contributors · Part of the async-kit ecosystem