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

idempotency-key

v0.1.0

Published

Run a request handler at most once per Idempotency-Key, across replicas. Reserve-then-run against Redis or Postgres, with response replay and fingerprint checking — not check-then-run, which is the bug it exists to prevent.

Readme

idempotency-key

A retried request must not charge the card twice.

The middleware everyone writes looks like this, and it is wrong:

const cached = await store.get(key);
if (cached) return cached;        // <- the check
const result = await handler();   // <- and the gap
await store.set(key, result);

Two requests arriving together both see nothing cached, and both run. Measured with three concurrent calls sharing one key:

check-then-run   : handler ran 3 times for one key
reserve-then-run : handler ran 1 time

The fix is to make the reservation itself atomic, in a store all your replicas share — because an in-process Map closes the gap inside one process and three pods still charge three times.

npm install idempotency-key

Use

import { Idempotency, RedisStore, fingerprint } from 'idempotency-key';

const idem = new Idempotency({ store: new RedisStore(redis) });

app.post('/charges', async (req, res) => {
  const key = req.headers['idempotency-key'];
  const result = await idem.run(
    key,
    { fingerprint: fingerprint({ method: 'POST', path: req.path, body: req.rawBody }) },
    async () => {
      const charge = await chargeTheCard(req.body);
      return { status: 201, body: JSON.stringify(charge) };
    },
  );

  res.status(result.status);
  if (result.replayed) res.set('Idempotent-Replay', 'true');
  res.send(result.body);
});

Framework-agnostic on purpose: it takes a key and a handler and gives you back a status, headers and a body. Express, Fastify, Hono, a Lambda, a queue consumer — none of them are mentioned anywhere in the package.

Stores

| | reservation | atomic because | |---|---|---| | RedisStore | SET key NX EX ttl | the check and the write are one command | | PostgresStore | INSERT … ON CONFLICT DO NOTHING … RETURNING | one statement returns a row only to the inserter | | MemoryStore | a Map | it is not, across replicas — named so nobody deploys it by accident |

reserve is the only operation the interface specifies as atomic, and it is the only one that matters. Everything else is bookkeeping around it. A store that implements it as a read followed by a write passes every other test in this repository and loses the race in production, which is why the same suite runs against all three.

The Postgres store has a second advantage worth naming: the reservation lives in the same database as your business write, so a handler writing in the same transaction cannot leave a key saying "done" for work that rolled back.

The four decisions

Concurrent duplicate → 409, or wait. Default is 409 Conflict, which is what Stripe does and what a well-behaved client retries. onInFlight: 'wait' blocks until the winner finishes and replays its response, which is friendlier and can hold a connection for as long as the handler takes.

Same key, different body → 422. That is a client bug, not a retry, and replaying the first response would answer a question nobody asked. The fingerprint covers method, path and body — deliberately not headers, which carry tracing ids and timestamps that differ between two retries of the same request and would turn every legitimate retry into a 422.

5xx is not remembered; 4xx is. A 422 for a malformed body will be a 422 next time, so replaying it is correct. A 500 is usually transient, and caching it would make every retry return the same 500 for the whole TTL — an outage that outlives its cause, produced by the code meant to make retries safe. Override with cacheErrors.

A thrown handler releases the key. It produced no response to remember, and holding the reservation would lock the key for 24 hours over a blip.

What it does not do

It does not make your handler idempotent. It runs it at most once per key. If the handler itself writes twice, or writes and then fails before the response is stored, that is still yours to get right — the usual answer is a transactional outbox, and the Postgres store exists partly so both can share one transaction.

It does not generate keys. The client owns the key, because the client is what knows that two requests are the same attempt. A server-generated key is a request id with extra steps.

It does not sweep Postgres for you. PostgresStore.sweep() deletes expired rows; run it on a timer. Nothing depends on it for correctness — an expired key is overwritten by the next caller rather than blocking one.

Tests

npm install
npm run infra:up
npm test          # 30 tests: the same 10 against all three stores
npm run infra:down

The one to read first is "runs the handler exactly once for concurrent duplicates". A check-then-run implementation passes every other test in the file and fails that one, which is also the only test that resembles what happens in production.

Built with Claude

Claude wrote most of this code. The design is mine, and so is the decision to measure the problem before writing any of it: the three-runs-for-one-key result at the top of this README came from a probe, not from a blog post.

Licence

MIT.