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

idempotent-core

v1.0.0

Published

Bulletproof, zero-dependency idempotency middleware for Node.js. Eliminates race conditions on critical endpoints (payments, webhooks) for Express, Fastify, and native HTTP.

Readme

IdempotentCore

🇫🇷 Lire en français | 🇬🇧 You are reading the English version

npm version CI license: MIT dependencies: 0 node PRs welcome

Bulletproof, zero-dependency idempotency middleware for Node.js. Completely eliminates race conditions on your critical endpoints (Stripe payments, Mobile Money, webhooks, destructive actions) — compatible with Express, Fastify, and native http.


📊 Project dashboard

| Metric | Value | |---|---| | 📦 Production dependencies | 0 — only node:crypto, node:async_hooks, node:events | | ⚡️ Measured average overhead / request | < 0.5 ms (see benchmarks) | | 🧪 Test status | See CI badge above (Node 18 / 20 / 22 — Ubuntu & Windows) | | 🧩 Supported frameworks | Express, Fastify, native http | | 🔒 License | MIT — free for commercial use | | 🗂️ Package size | A few KB (nothing to download, zero deps) | | 🖥️ OS tested in CI | Ubuntu, Windows |


Why this package exists

Two identical requests hit /charge at the same time (double-click, network retry, webhook retry from a payment provider). Without protection: two charges, two emails, two database rows. IdempotentCore guarantees that only one logical request actually executes, even under heavy concurrency, without touching an external database or a distributed lock system — everything happens in native V8 memory, with sub-millisecond overhead.

Key features

  • 🔒 Atomic locking: a concurrent duplicate request receives a clean 409 Conflict instantly — double execution is structurally impossible.
  • ⚡️ Zero dependencies: only node:crypto, node:async_hooks, node:events. No node_modules to audit.
  • 🧠 Automatic deterministic fingerprinting: no need to force an Idempotency-Key header on the client — a stable SHA-256 hash (method + URL + user + normalized body) protects even third-party webhooks you don't control.
  • 🔁 Instant replay: any duplicate request received AFTER processing finished gets the exact cached response, byte for byte — no re-execution.
  • 🧵 Full async traceability via AsyncLocalStorage: the idempotency context flows through every await, callback, and logging layer with no extra parameter to thread through.
  • 🧹 Automatic memory cleanup (TTL sweep): no memory-leak risk, even after millions of requests.
  • 🧩 Pluggable storage: native in-memory by default, with an open interface to plug in Redis/Memcached for multi-instance environments.
  • 🛡️ Safe by default: a 5xx response is never cached — the lock is released to allow a real retry; only success/client-error responses (< 500) are frozen.
  • 📡 Native observability: events (lock:acquired, lock:conflict, response:cached, response:replayed, engine:error...) to wire up Prometheus/logs metrics without imposing any dependency.

Installation

npm install idempotent-core

No dependency will be installed — the package is 100% zero-dep.

Quick start (Express)

const express = require('express');
const { idempotency } = require('idempotent-core');

const app = express();
app.use(express.json());

app.post(
  '/api/charge',
  idempotency({ ttl: 24 * 60 * 60 * 1000 }), // cache 24h, Stripe-style
  async (req, res) => {
    // Your critical logic here: Stripe call, DB write, etc.
    const charge = await stripe.charges.create({ amount: req.body.amount });
    res.status(201).json({ success: true, chargeId: charge.id });
  }
);

app.listen(3000);

That's it. Two identical concurrent requests → only one executes, the other gets a 409. A third request afterward → the cached response is replayed instantly.

Not just Stripe: works with any provider

The middleware knows nothing about what it protects — it just captures the response and blocks duplicates. It works identically with Orange Money, Wave, MTN Mobile Money, CinetPay, PayPal, a bank transfer, or even a plain account creation:

app.post(
  '/api/mobile-money/pay',
  idempotency({ ttl: 5 * 60 * 1000 }), // 5 min cache
  async (req, res) => {
    const { phoneNumber, amount, operator } = req.body;
    const tx = await mobileMoneyProvider.initiate({ phoneNumber, amount, operator });
    res.status(201).json({ transactionId: tx.id });
  }
);

If the client double-clicks or their network retries the request (common with unstable connections), only one charge goes through — guaranteed.

Using an explicit header (Stripe-style)

POST /api/charge HTTP/1.1
Idempotency-Key: 7f3a9c2e-... (client-generated)
Content-Type: application/json

{ "amount": 4999 }

If the Idempotency-Key header is present, it's used as-is for the lock key — automatic fingerprinting only kicks in when the header is absent.

Using native http

const http = require('http');
const { idempotency } = require('idempotent-core');

const middleware = idempotency({ ttl: 60_000 });

const server = http.createServer((req, res) => {
  let body = '';
  req.on('data', (c) => (body += c));
  req.on('end', () => {
    req.body = body ? JSON.parse(body) : {};
    middleware(req, res, (err) => {
      if (err) {
        res.writeHead(500).end(JSON.stringify({ error: 'internal' }));
        return;
      }
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ ok: true }));
    });
  });
});

server.listen(4000);

Using Fastify

See examples/fastify-example.js — Fastify exposes reply.raw, the actual http.ServerResponse our middleware operates on directly.

Configuration options

| Option | Type | Default | Description | |---|---|---|---| | ttl | number | 86400000 (24h) | How long a COMPLETED response stays cached (ms). | | processingTtl | number | = ttl | Safety ceiling for a PROCESSING lock — guarantees a process crash never blocks an endpoint forever. | | sweepInterval | number | 30000 | Frequency (ms) of the background TTL sweep. 0 disables automatic sweeping. | | store | StoreInterface | MemoryStore | Custom storage backend (see Redis section below). | | engine | IdempotentCore | — | Reuse an existing instance (shares the lock table across multiple routers). | | headerName | string | 'idempotency-key' | Client header name for an explicit key. | | autoFingerprint | boolean | true | If false, requests without an explicit header are not protected. | | methods | string[] | ['POST','PUT','PATCH','DELETE'] | HTTP methods intercepted. | | isCacheable | (code:number)=>boolean | code < 500 | Decides whether a final status code should be cached or release the lock. | | userIdExtractor | (req)=>string | — | Custom user-id extraction for fingerprinting. |

Internal architecture

┌─────────────────────────────────────────────────────────────────┐
│                        idempotency() middleware                  │
│                                                                    │
│  1. Explicit key (header) OR automatic SHA-256 fingerprint         │
│  2. engine.acquire(key)  ──────────────►  MemoryStore (native Map) │
│         │                                                          │
│         ├── NEW         → wrap res.write/end/writeHead             │
│         │                  → AsyncLocalStorage.run(...)            │
│         │                  → next() → real business logic          │
│         │                  → capture response → engine.complete()  │
│         │                                                          │
│         ├── PROCESSING  → immediate 409 Conflict                   │
│         │                                                          │
│         └── COMPLETED   → replay of the cached payload              │
│                                                                    │
│  TTL Sweep (setInterval, unref'd): purges expired entries           │
└─────────────────────────────────────────────────────────────────┘

Extending storage (Redis, for multi-instance deployments)

By default, MemoryStore works within a single process. If your SaaS runs across multiple instances/pods behind a load balancer, implement StoreInterface on top of your existing Redis client (without adding Redis as a dependency of the package itself):

const { StoreInterface, idempotency } = require('idempotent-core');

class RedisStore extends StoreInterface {
  constructor(redisClient) {
    super();
    this.redis = redisClient;
  }
  async setIfNotExists(key, record, ttlMs) {
    const result = await this.redis.set(key, JSON.stringify(record), 'PX', ttlMs, 'NX');
    return result === 'OK';
  }
  async get(key) {
    const raw = await this.redis.get(key);
    return raw ? JSON.parse(raw) : null;
  }
  async update(key, record, ttlMs) {
    await this.redis.set(key, JSON.stringify(record), 'PX', ttlMs);
    return true;
  }
  async delete(key) {
    return (await this.redis.del(key)) > 0;
  }
  async size() {
    return this.redis.dbsize();
  }
}

app.use(idempotency({ store: new RedisStore(myRedisClient) }));

Observability

const { idempotency, IdempotentCore } = require('idempotent-core');

const engine = new IdempotentCore({ ttl: 60_000 });
engine.on('lock:conflict', ({ key }) => metrics.increment('idempotency.conflict'));
engine.on('response:replayed', ({ key, statusCode }) => metrics.increment('idempotency.replay'));
engine.on('engine:error', (err) => logger.error('idempotency engine error', err));

app.use(idempotency({ engine }));

// Health endpoint
app.get('/health/idempotency', (req, res) => res.json(engine.stats()));

Testing the concurrency proof

Quick test (no dependency, works on Windows/macOS/Linux):

npm test

This runs test/sanity-check.js, which simulates concurrent requests against minimal fake req/res objects and automatically asserts (via assert): exactly one 201 + one 409, exactly one real execution, and an instant replay with no re-execution.

Full demo with a real Express server (2 terminals):

npm install --no-save express   # only to run the demo
npm run test:demo:server        # Terminal 1 — leave it running
npm run test:demo:client        # Terminal 2 — fires the concurrent requests

The client sends two identical concurrent requests, then a third one afterward, and prints a PASS/FAIL report proving:

  • only one real execution happens (chargeCounter === 1),
  • the concurrent request correctly gets a 409,
  • the late request gets a replayed response in under 100ms.

Production security & robustness

  • Fail-open: if the engine itself fails (e.g. Redis unavailable), the request is let through unprotected rather than taking down all traffic — an engine:error event is emitted for alerting.
  • No lock leaks: client disconnect (res.close), synchronous exception, or process crash → the lock is released or expires automatically via processingTtl.
  • No caching of server errors: a 5xx releases the lock instead of being cached, so a legitimately retrying client never gets a stale failure replayed forever.
  • Binary buffer support: response capture also works for streamed/chunked payloads (Buffer concatenation).

📈 Benchmarks

Measured with test/sanity-check.js over 5000 consecutive unique requests (Node 20, standard dev machine):

| Scenario | Result | |---|---| | Average overhead per request (new key) | ~0.1 ms | | Blocked duplicate request (409) | < 1 ms (native Map lookup, no I/O) | | Replayed cached response | < 5 ms (no business-logic re-execution) |

These numbers are for the default in-memory backend (MemoryStore). With a remote backend (Redis), overhead naturally depends on network latency to your store.

🆚 Why not just roll my own?

| | Typical hand-rolled solution | IdempotentCore | |---|---|---| | Race condition on concurrent locking | Often overlooked (naive if (cache.has(key)) is not atomic) | Guaranteed atomic locking | | Memory leaks | Frequent (no TTL, no sweep) | Built-in automatic TTL sweep | | Streaming/chunked responses | Rarely handled | Captured via write/end interception | | Async traceability | Manual, fragile | Built-in AsyncLocalStorage | | Caching server errors | A common hidden trap | 5xx never cached by default | | Dependencies | Varies | 0 |

🗺️ Roadmap

  • [ ] Official Redis adapter published separately (idempotent-core-redis)
  • [ ] Native Fastify plugin (fastify-plugin) without manual reply.hijack() wiring
  • [ ] CLI diagnostics dashboard (npx idempotent-core stats)
  • [ ] Formal comparative benchmarks (autobench + chart)

Have an idea or a specific need? Open an issue.

🤝 Contributing

Contributions are welcome! See CONTRIBUTING.md for instructions (tests, code style, PR process).

License

MIT — see LICENSE