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

job-retry

v0.1.1

Published

Retry async functions with exponential backoff, per-attempt timeout, and a dead letter queue

Readme

job-retry

Retry any async function with exponential backoff, per-attempt timeout control, and a dead letter queue so permanently failed jobs are never silently lost.

import { JobRetry } from 'job-retry';

const runner = new JobRetry({ attempts: 5, backoff: 'exponential', baseDelay: 1000, jitter: true });

const result = await runner.run('sendEmail', () => sendEmail(user));

Install

npm install job-retry

For the Redis DLQ backend, also install ioredis:

npm install ioredis

Quick start

import { JobRetry } from 'job-retry';

const runner = new JobRetry({
  attempts: 5,
  backoff: 'exponential',
  baseDelay: 1000,
  timeout: 5000,
  jitter: true,
  dlq: 'memory',
  onRetry: (error, attempt) => console.log(`Attempt ${attempt} failed`, error),
  onFailure: (job) => console.error('Job permanently failed', job),
  onSuccess: (result, attempts) => console.log(`Succeeded after ${attempts} tries`),
});

// Run a job — retries automatically on failure
const result = await runner.run('sendEmail', () => sendEmail(user));

// Inspect the dead letter queue
const failed = await runner.dlq.getAll();

// Retry a failed job after you've fixed the underlying issue
await runner.dlq.retry(failed[0].id, runner);

// Remove a single entry or wipe everything
await runner.dlq.remove(failed[0].id);
await runner.dlq.clear();

Options

| Option | Type | Default | Description | |---|---|---|---| | attempts | number | 3 | Maximum attempts before the job is moved to the DLQ | | backoff | 'fixed' \| 'linear' \| 'exponential' | 'exponential' | Delay strategy between retries | | baseDelay | number | 1000 | Base delay in milliseconds | | timeout | number | none | Per-attempt timeout in ms — hanging attempts throw TimeoutError | | jitter | boolean | false | Adds random delay (up to 1× baseDelay) to prevent thundering herd | | dlq | 'memory' \| 'file' \| 'redis' \| DLQBackend | 'memory' | Dead letter queue backend | | dlqFilePath | string | './job-retry-dlq.json' | File path for the file backend | | dlqRedisClient | Redis | — | ioredis client instance for the Redis backend | | onRetry | (error, attempt) => void | — | Called after each failed attempt except the last | | onFailure | (job: DLQEntry) => void | — | Called when the job is moved to the DLQ | | onSuccess | (result, attempts) => void | — | Called on success when retries were needed |


Backoff strategies

Fixed — waits baseDelay every attempt.

Linear — waits baseDelay × attempt (1s, 2s, 3s, …).

Exponential — waits baseDelay × 2^(attempt−1) (1s, 2s, 4s, 8s, …).

Jitter — adds random(0, delay) to the computed delay. Prevents multiple jobs from retrying at the exact same instant after a shared outage (thundering herd).


Dead letter queue backends

Memory (default)

new JobRetry({ dlq: 'memory' })

Stored in an in-process array. Lost on restart. Good for development and testing.

File

new JobRetry({
  dlq: 'file',
  dlqFilePath: './failed-jobs.json',
})

Persisted to a JSON file on disk. Survives restarts. Good for single-server apps.

Redis

import Redis from 'ioredis';

new JobRetry({
  dlq: 'redis',
  dlqRedisClient: new Redis(),
})

Stored as Redis hashes with a list for ordering. Shared across multiple servers, survives restarts. Production-ready.

Custom backend

Implement the DLQBackend interface and pass the instance directly:

import type { DLQBackend, DLQEntry } from 'job-retry';

class MyDLQ implements DLQBackend {
  async push(entry: DLQEntry): Promise<void> { /* ... */ }
  async getAll(): Promise<DLQEntry[]> { /* ... */ }
  async get(id: string): Promise<DLQEntry | null> { /* ... */ }
  async retry(id: string, runner: JobRetry): Promise<unknown> { /* ... */ }
  async remove(id: string): Promise<void> { /* ... */ }
  async clear(): Promise<void> { /* ... */ }
  async size(): Promise<number> { /* ... */ }
}

new JobRetry({ dlq: new MyDLQ() })

DLQ API

| Method | Description | |---|---| | dlq.getAll() | Returns all entries in the queue | | dlq.get(id) | Returns a single entry by ID, or null | | dlq.retry(id, runner) | Re-runs the original function and removes the entry on success | | dlq.remove(id) | Deletes an entry from the queue | | dlq.clear() | Empties the entire queue | | dlq.size() | Returns the number of entries |


Error types

import { MaxAttemptsExceededError, TimeoutError } from 'job-retry';

try {
  await runner.run('job', fn);
} catch (err) {
  if (err instanceof MaxAttemptsExceededError) {
    console.log(`Failed after ${err.attempts} attempts`);
    // err.cause holds the last underlying error
  }
}

TimeoutError is thrown internally when a per-attempt timeout fires. It becomes the cause on MaxAttemptsExceededError.


TypeScript

Full types ship with the package — no @types/job-retry needed.

import type { RetryOptions, DLQEntry, DLQBackend, BackoffStrategy } from 'job-retry';

License

MIT