retry vs p-retry vs exponential-backoff vs async-retry vs promise-retry vs backoff
Implementing Resilient Retry Logic in JavaScript Applications
retryp-retryexponential-backoffasync-retrypromise-retrybackoffSimilar Packages:

Implementing Resilient Retry Logic in JavaScript Applications

These six libraries provide mechanisms to automatically retry failed operations, such as network requests or database queries, to improve application resilience. While they share the same goal, they differ significantly in their API style (callbacks vs. promises), backoff strategies, and maintenance status. retry is the foundational callback-based library, while async-retry, p-retry, and promise-retry offer modern Promise-based interfaces. backoff and exponential-backoff focus specifically on the timing algorithms behind the retries. Choosing the right one depends on whether your codebase uses async/await, how much control you need over delay timing, and whether you require active maintenance.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
retry74,684,2311,256-275 years agoMIT
p-retry33,907,4191,03825.8 kB12 months agoMIT
exponential-backoff25,560,67440855.2 kB7a year agoApache-2.0
async-retry22,270,0271,918-305 years agoMIT
promise-retry16,924,649318-116 years agoMIT
backoff681,737336-1111 years agoMIT

Implementing Resilient Retry Logic: A Deep Dive into 6 JavaScript Libraries

Network failures, database locks, and temporary API rate limits are facts of life in distributed systems. To handle them, developers use retry logic — automatically attempting a failed operation again after a short delay. The JavaScript ecosystem offers several tools for this, ranging from low-level backoff algorithms to high-level Promise wrappers. Let's compare async-retry, backoff, exponential-backoff, p-retry, promise-retry, and retry to see which one fits your architecture.

🏗️ API Style: Callbacks vs. Promises

The biggest divide in this group is between older callback-based libraries and modern Promise-based ones. This choice dictates how your code looks and how easily it integrates with async/await.

retry is the original foundation. It uses callbacks exclusively.

  • You pass a function and a callback.
  • The callback receives an error if all retries fail.
  • Harder to read in modern codebases.
// retry: Callback-based
const retry = require('retry');

const operation = retry.operation({ retries: 3 });

operation.attempt(function(currentAttempt) {
  fetchData(function(err, result) {
    if (operation.retry(err)) {
      return;
    }
    console.error(operation.mainError());
  });
});

backoff also leans heavily on callbacks and events.

  • You create a backoff instance and listen for a backoff event.
  • You manually trigger the next attempt inside the event handler.
  • Gives you control but requires more boilerplate.
// backoff: Event-based
const backoff = require('backoff');

const call = backoff.call(fetchData, arg1, arg2);

call.setStrategy(new backoff.ExponentialStrategy());

call.on('backoff', function(number, delay) {
  console.log(`Waiting ${delay}ms before retry #${number}`);
});

call.on('callback', function(err, result) {
  console.log('Done:', err || result);
});

call.start();

p-retry wraps the original retry module in a Promise.

  • Returns a Promise that resolves with the result or rejects after max attempts.
  • Clean async/await syntax.
// p-retry: Promise-based
import pRetry from 'p-retry';

const result = await pRetry(async () => {
  return await fetchData();
}, {
  retries: 3
});

async-retry is built for Promises from the ground up.

  • Maintained by Vercel, designed for serverless functions.
  • Very concise syntax, passes a bail function to stop retries early.
// async-retry: Promise-based
import retry from 'async-retry';

const result = await retry(async (bail) => {
  const res = await fetchData();
  if (res.status === 404) {
    bail(new Error('Not Found')); // Stop retrying immediately
  }
  return res;
}, {
  retries: 3
});

promise-retry is similar to p-retry but with npm-style error filtering.

  • Returns a Promise.
  • Allows you to filter which errors should trigger a retry based on error codes.
// promise-retry: Promise-based
const promiseRetry = require('promise-retry');

const result = await promiseRetry((retry, number) => {
  return fetchData().catch(retry);
}, {
  retries: 3,
  factor: 2
});

exponential-backoff focuses on the timing algorithm but supports Promises.

  • Provides a backOff function that wraps your async operation.
  • Highly configurable timing, less focus on error filtering logic.
// exponential-backoff: Promise-based
import { backOff } from 'exponential-backoff';

const result = await backOff(async () => {
  return await fetchData();
}, {
  numOfAttempts: 3
});

⏱️ Backoff Strategies: How Long to Wait

When a request fails, you shouldn't retry immediately. Waiting longer between each attempt (exponential backoff) prevents overwhelming the server. Some libraries let you customize this; others have fixed defaults.

retry uses a customizable exponential formula.

  • You define retries, factor, minTimeout, and maxTimeout.
  • Good balance of control and simplicity.
// retry: Custom timeouts
const operation = retry.operation({
  retries: 3,
  factor: 2,
  minTimeout: 1000,
  maxTimeout: 5000
});

backoff allows you to swap strategies entirely.

  • Comes with ExponentialStrategy, FibonacciStrategy, and custom implementations.
  • Best if you need non-standard timing (like Fibonacci).
// backoff: Pluggable strategies
const call = backoff.call(fn);
call.setStrategy(new backoff.FibonacciStrategy());

exponential-backoff is the most strict about the algorithm.

  • Supports jitter (randomness) out of the box to prevent thundering herd problems.
  • Ideal for high-traffic APIs where many clients might retry at once.
// exponential-backoff: Built-in jitter
await backOff(fn, {
  jitter: 'full', // Adds randomness to delay
  startingDelay: 500,
  timeMultiple: 2
});

p-retry, async-retry, and promise-retry inherit or mimic standard exponential defaults.

  • They focus more on the flow control than the math.
  • Usually sufficient for standard web requests.
// p-retry: Standard options
await pRetry(fn, {
  retries: 3,
  factor: 2,
  minTimeout: 1000
});

🛑 Stopping Early: When to Give Up

Sometimes a retry is pointless. If a user is not found (404), retrying won't help. You need a way to break the loop immediately.

async-retry makes this explicit with a bail function.

  • Pass bail as an argument to your async function.
  • Call bail(error) to stop retries and reject immediately.
  • Very clear intent in code.
// async-retry: Explicit bail
await retry(async (bail) => {
  const res = await api.getUser(id);
  if (res.status === 404) {
    bail(new Error('User missing')); // Stops retries
  }
  return res;
});

p-retry handles this by throwing specific errors.

  • You can throw an error that matches a filter to stop.
  • Or use the shouldRetry option in newer versions to return false.
// p-retry: Filter based
await pRetry(async () => {
  const res = await api.getUser(id);
  if (res.status === 404) {
    const err = new Error('User missing');
    err.doNotRetry = true; // Custom flag to stop
    throw err;
  }
  return res;
});

promise-retry uses error codes.

  • Checks err.code against a list of retryable errors.
  • If the code isn't in the list, it stops.
// promise-retry: Code based
await promiseRetry((retry, number) => {
  return fetchData().catch(err => {
    if (err.code === 'ENOTFOUND') {
      throw err; // Won't retry this specific code if filtered
    }
    retry(err);
  });
}, { retries: 3 });

retry, backoff, and exponential-backoff require manual logic.

  • You must check the error inside your operation function.
  • If it's fatal, you call the final callback or throw without triggering the next backoff.
// retry: Manual check
operation.attempt(function(currentAttempt) {
  fetchData(function(err, result) {
    if (err && err.code === 'NOT_RETRYABLE') {
      // Manually fail without retrying
      console.error(err);
      return;
    }
    if (operation.retry(err)) {
      return;
    }
  });
});

🛠️ Maintenance and Ecosystem

Not all libraries are kept up to date. Using an unmaintained package can lead to security risks or incompatibility with newer Node.js versions.

  • async-retry: Actively maintained by Vercel. Safe for production. Widely used in serverless.
  • p-retry: Actively maintained by Sindre Sorhus. Very stable. Great for frontend and Node.
  • exponential-backoff: Active. Good choice if you need strict backoff compliance.
  • promise-retry: Maintained but more niche. Often seen in npm internals.
  • retry: Stable but older. Callback style is dated.
  • backoff: Less common in modern web apps. More for low-level network tools.

⚠️ Warning: Avoid retry and backoff for new frontend projects. The callback pattern makes code harder to read and test compared to Promise-based alternatives.

📊 Summary Table

PackageStyleBackoff ControlBail/Stop LogicBest For
async-retryPromiseStandard✅ bail() functionServerless, Modern Node
p-retryPromiseStandard⚠️ Error flagsGeneral Web Apps
exponential-backoffPromise✅ Advanced (Jitter)⚠️ Error throwingHigh-traffic APIs
promise-retryPromiseStandard⚠️ Error codesTooling, npm-like tasks
retryCallbackStandard⚠️ Manual checkLegacy Code
backoffCallback/Event✅ Pluggable Strategies⚠️ Manual checkLow-level Network Tools

💡 Final Recommendation

For most modern frontend and Node.js developers, async-retry or p-retry are the best choices.

  • Pick async-retry if you like the bail pattern for clearly stopping retries on fatal errors. It is concise and well-maintained.
  • Pick p-retry if you want a minimal wrapper that feels like a native Promise utility.
  • Pick exponential-backoff only if you are building a high-scale client where jitter and strict timing are critical to avoid rate limiting.

Avoid retry and backoff unless you are working on legacy systems. The Promise-based tools reduce boilerplate and integrate seamlessly with async/await, making your error handling logic easier to follow and maintain.

How to Choose: retry vs p-retry vs exponential-backoff vs async-retry vs promise-retry vs backoff

  • retry:

    Choose retry only if you are maintaining legacy code that relies on callbacks or if you need the absolute base layer upon which other libraries are built. For new projects, prefer a Promise-based alternative like p-retry or async-retry to avoid callback hell.

  • p-retry:

    Choose p-retry if you are already using the Sindre Sorhus ecosystem or prefer a minimal, focused Promise wrapper around the original retry module. It is excellent for frontend or Node.js projects that need a simple await interface with sensible defaults and abort signal support.

  • exponential-backoff:

    Choose exponential-backoff if your primary concern is a highly configurable, standards-compliant exponential backoff algorithm with jitter. It is best for scenarios where network congestion is a major concern and you need to adhere to specific retry-after headers or strict timing requirements.

  • async-retry:

    Choose async-retry if you want a lightweight, Promise-based wrapper that is actively maintained by Vercel. It is ideal for serverless environments or modern Node.js applications where you need simple retry logic with exponential backoff without extra configuration overhead.

  • promise-retry:

    Choose promise-retry if you need compatibility with npm-style retry logic or require specific handling for error codes (like ENOTFOUND). It is often used in tooling or package managers but may be overkill for standard application HTTP requests.

  • backoff:

    Choose backoff if you are working with legacy callback-based code or streams and need fine-grained control over the backoff strategy itself. It is less suitable for new Promise-based projects but remains useful for low-level network tools or when you need to separate the backoff logic from the retry execution.

README for retry

Build Status codecov

retry

Abstraction for exponential and custom retry strategies for failed operations.

Installation

npm install retry

Current Status

This module has been tested and is ready to be used.

Tutorial

The example below will retry a potentially failing dns.resolve operation 10 times using an exponential backoff strategy. With the default settings, this means the last attempt is made after 17 minutes and 3 seconds.

var dns = require('dns');
var retry = require('retry');

function faultTolerantResolve(address, cb) {
  var operation = retry.operation();

  operation.attempt(function(currentAttempt) {
    dns.resolve(address, function(err, addresses) {
      if (operation.retry(err)) {
        return;
      }

      cb(err ? operation.mainError() : null, addresses);
    });
  });
}

faultTolerantResolve('nodejs.org', function(err, addresses) {
  console.log(err, addresses);
});

Of course you can also configure the factors that go into the exponential backoff. See the API documentation below for all available settings. currentAttempt is an int representing the number of attempts so far.

var operation = retry.operation({
  retries: 5,
  factor: 3,
  minTimeout: 1 * 1000,
  maxTimeout: 60 * 1000,
  randomize: true,
});

API

retry.operation([options])

Creates a new RetryOperation object. options is the same as retry.timeouts()'s options, with three additions:

  • forever: Whether to retry forever, defaults to false.
  • unref: Whether to unref the setTimeout's, defaults to false.
  • maxRetryTime: The maximum time (in milliseconds) that the retried operation is allowed to run. Default is Infinity.

retry.timeouts([options])

Returns an array of timeouts. All time options and return values are in milliseconds. If options is an array, a copy of that array is returned.

options is a JS object that can contain any of the following keys:

  • retries: The maximum amount of times to retry the operation. Default is 10. Seting this to 1 means do it once, then retry it once.
  • factor: The exponential factor to use. Default is 2.
  • minTimeout: The number of milliseconds before starting the first retry. Default is 1000.
  • maxTimeout: The maximum number of milliseconds between two retries. Default is Infinity.
  • randomize: Randomizes the timeouts by multiplying with a factor between 1 to 2. Default is false.

The formula used to calculate the individual timeouts is:

Math.min(random * minTimeout * Math.pow(factor, attempt), maxTimeout)

Have a look at this article for a better explanation of approach.

If you want to tune your factor / times settings to attempt the last retry after a certain amount of time, you can use wolfram alpha. For example in order to tune for 10 attempts in 5 minutes, you can use this equation:

screenshot

Explaining the various values from left to right:

  • k = 0 ... 9: The retries value (10)
  • 1000: The minTimeout value in ms (1000)
  • x^k: No need to change this, x will be your resulting factor
  • 5 * 60 * 1000: The desired total amount of time for retrying in ms (5 minutes)

To make this a little easier for you, use wolfram alpha to do the calculations:

http://www.wolframalpha.com/input/?i=Sum%5B1000*x^k%2C+{k%2C+0%2C+9}%5D+%3D+5+*+60+*+1000

retry.createTimeout(attempt, opts)

Returns a new timeout (integer in milliseconds) based on the given parameters.

attempt is an integer representing for which retry the timeout should be calculated. If your retry operation was executed 4 times you had one attempt and 3 retries. If you then want to calculate a new timeout, you should set attempt to 4 (attempts are zero-indexed).

opts can include factor, minTimeout, randomize (boolean) and maxTimeout. They are documented above.

retry.createTimeout() is used internally by retry.timeouts() and is public for you to be able to create your own timeouts for reinserting an item, see issue #13.

retry.wrap(obj, [options], [methodNames])

Wrap all functions of the obj with retry. Optionally you can pass operation options and an array of method names which need to be wrapped.

retry.wrap(obj)

retry.wrap(obj, ['method1', 'method2'])

retry.wrap(obj, {retries: 3})

retry.wrap(obj, {retries: 3}, ['method1', 'method2'])

The options object can take any options that the usual call to retry.operation can take.

new RetryOperation(timeouts, [options])

Creates a new RetryOperation where timeouts is an array where each value is a timeout given in milliseconds.

Available options:

  • forever: Whether to retry forever, defaults to false.
  • unref: Wether to unref the setTimeout's, defaults to false.

If forever is true, the following changes happen:

  • RetryOperation.errors() will only output an array of one item: the last error.
  • RetryOperation will repeatedly use the timeouts array. Once all of its timeouts have been used up, it restarts with the first timeout, then uses the second and so on.

retryOperation.errors()

Returns an array of all errors that have been passed to retryOperation.retry() so far. The returning array has the errors ordered chronologically based on when they were passed to retryOperation.retry(), which means the first passed error is at index zero and the last is at the last index.

retryOperation.mainError()

A reference to the error object that occured most frequently. Errors are compared using the error.message property.

If multiple error messages occured the same amount of time, the last error object with that message is returned.

If no errors occured so far, the value is null.

retryOperation.attempt(fn, timeoutOps)

Defines the function fn that is to be retried and executes it for the first time right away. The fn function can receive an optional currentAttempt callback that represents the number of attempts to execute fn so far.

Optionally defines timeoutOps which is an object having a property timeout in miliseconds and a property cb callback function. Whenever your retry operation takes longer than timeout to execute, the timeout callback function cb is called.

retryOperation.try(fn)

This is an alias for retryOperation.attempt(fn). This is deprecated. Please use retryOperation.attempt(fn) instead.

retryOperation.start(fn)

This is an alias for retryOperation.attempt(fn). This is deprecated. Please use retryOperation.attempt(fn) instead.

retryOperation.retry(error)

Returns false when no error value is given, or the maximum amount of retries has been reached.

Otherwise it returns true, and retries the operation after the timeout for the current attempt number.

retryOperation.stop()

Allows you to stop the operation being retried. Useful for aborting the operation on a fatal error etc.

retryOperation.reset()

Resets the internal state of the operation object, so that you can call attempt() again as if this was a new operation object.

retryOperation.attempts()

Returns an int representing the number of attempts it took to call fn before it was successful.

License

retry is licensed under the MIT license.

Changelog

0.10.0 Adding stop functionality, thanks to @maxnachlinger.

0.9.0 Adding unref functionality, thanks to @satazor.

0.8.0 Implementing retry.wrap.

0.7.0 Some bug fixes and made retry.createTimeout() public. Fixed issues #10, #12, and #13.

0.6.0 Introduced optional timeOps parameter for the attempt() function which is an object having a property timeout in milliseconds and a property cb callback function. Whenever your retry operation takes longer than timeout to execute, the timeout callback function cb is called.

0.5.0 Some minor refactoring.

0.4.0 Changed retryOperation.try() to retryOperation.attempt(). Deprecated the aliases start() and try() for it.

0.3.0 Added retryOperation.start() which is an alias for retryOperation.try().

0.2.0 Added attempts() function and parameter to retryOperation.try() representing the number of attempts it took to call fn().