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

durability

v2.0.0

Published

Alarm-backed durable operations for Cloudflare Durable Objects

Readme

Durability

Alarm-backed, effectively-once operation execution for Cloudflare Durable Objects.

The package registers each operation and the first alarm in one Durable Object storage transaction. It only creates an alarm when none exists. Generated operation methods return Promise<void> after registration commits; they do not wait for the handler result. An alarm firing during execution attaches to the same in-memory promise. Failed calls are retried from later alarms, and a stable call ID deduplicates completed and concurrent calls. SQLite access and schema migrations are managed through workers-qb.

import { DurableObject } from 'cloudflare:workers';
import { createDurability, type DurableHandler } from 'durability';

type ResizeInput = { imageId: string };

export class ImageJobs extends DurableObject<Env> {
  private readonly durability = createDurability(this.ctx, {
    resizeImage: (async ({ id, payload }) => {
      return this.env.IMAGES.resize(payload.imageId, { idempotencyKey: id });
    }) satisfies DurableHandler<ResizeInput, string>,
  });

  resize(imageId: string) {
    return this.durability.resizeImage({
      id: `resize:${imageId}`,
      payload: { imageId },
    });
  }

  alarm(alarmInfo?: AlarmInvocationInfo) {
    return this.durability.alarm(alarmInfo);
  }
}

SQLite requirement

The Durable Object class must use SQLite storage:

{
  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["ImageJobs"] }],
}

createDurability migrates to the latest schema and stores calls in the durability_calls table. Each package migration has up and down SQL and is tracked in the namespaced durability_migrations table.

Rollbacks are explicit because rolling back the initial migration deletes durable call records:

import { migrateDurability } from 'durability';

migrateDurability(this.ctx, 'durability_0001_create_calls');
migrateDurability(this.ctx, null); // roll back every durability migration

Passing a migration name moves the schema to that exact version, applying or reverting migrations as needed. Payloads and results must be JSON-serializable.

Reading results

Results are read through the same typed operation using its idempotency key:

const result = await this.durability.resizeImage.getResult(`resize:${imageId}`);

switch (result.status) {
  case 'not_found':
    break;
  case 'pending':
    console.log(result.attempt, result.nextAttemptAt, result.lastError);
    break;
  case 'failed':
    console.error(result.error.name, result.error.message);
    break;
  case 'completed':
    console.log(result.result);
    break;
}

The completed result type is inferred from the operation handler. Looking up a key belonging to another operation throws DuplicateDurableCallError rather than returning a result with the wrong type.

Retries, timeouts, and terminal failures

Attempts use exponential backoff with equal jitter, stop after five attempts, and time out after five minutes by default. The retry delay function fully controls scheduling and can be overridden globally or per operation. The package exports the default delay building blocks for custom policies:

import { exponential, jitter } from 'durability/utils';

const durability = createDurability(this.ctx, handlers, {
  attemptTimeoutMs: 60_000,
  retries: {
    maxAttempts: 5,
    delay: (attempt) => jitter(exponential(attempt)),
  },
  methods: {
    resizeImage: {
      attemptTimeoutMs: 10 * 60_000,
      retries: {
        maxAttempts: 2,
        delay: (attempt) => exponential(attempt, 500, 30_000),
      },
    },
  },
});

Each attempt receives its own AbortSignal:

const handlers = {
  sendEmail: async ({ payload, signal }: DurableCall<EmailPayload>) =>
    fetch(payload.url, { method: 'POST', signal }),
};

A timed-out attempt aborts its signal and follows the normal retry policy. Abort-aware APIs stop promptly; arbitrary handler code cannot be forcibly terminated.

Throw NonRetryableError to move a call directly to failed without another attempt:

import { NonRetryableError } from 'durability';

throw new NonRetryableError('Recipient permanently rejected');

Errors created by NonRetryableError from cloudflare:workflows are also recognized.

Delivery semantics

Calls are delivered at least once and deduplicated by ID after completion. Arbitrary external side effects cannot be made strictly exactly-once: a process can stop after the side effect succeeds but before its completion record commits. Pass the call ID to external services as an idempotency key to obtain effectively-once behavior.

Completion records are retained indefinitely so IDs remain deduplicated. The helper owns the Durable Object's alarm; compose unrelated scheduled work through the same alarm handler instead of independently replacing its alarm.

Durable Object-to-Object coordination

When an operation spans multiple Durable Objects, a durability-enabled coordinator can retry each step after a crash, while durability-enabled participants deduplicate those retries using stable operation IDs. This supports sagas and other two-phase coordination scenarios with eventual consistency; it does not create one atomic storage transaction across the objects.

Intermediate states may be visible until every participant completes. Persist the coordination state, propagate stable IDs to every participant, and explicitly handle terminal failures or compensating operations when the operation can be aborted.

Long-running alarm calls

Alarm invocations have a 15-minute wall-time limit. If a handler is still pending after 14 minutes, the helper retains its promise in memory, arms an immediate alarm, and returns from the current invocation. The next alarm attaches to the same promise instead of starting the handler again:

const durability = createDurability(this.ctx, handlers, {
  alarmHandoffMs: 14 * 60_000,
});

This handoff can keep a call running beyond one alarm invocation while the Durable Object remains in memory. It is intentionally backed by the persisted pending call: if Cloudflare evicts or restarts the object during a handoff, the next alarm reconstructs and executes the call again. External side effects still require the stable call ID as an idempotency key.