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

conclimit

v0.1.0

Published

Distributed concurrency and rate limiting on Redis. A limit of 5 stays 5 across forty replicas. Works with ioredis and node-redis v4+, survives Redis Cluster and a crashed holder, and keeps no settings in Redis to lose.

Readme

conclimit

A limit of 5 should still be 5 when you scale to forty replicas.

p-limit and p-queue limit concurrency inside one process. Deploy forty pods and your limit of 5 is a limit of 200 — usually discovered by the third party you were trying not to overload.

import { Limiter } from 'conclimit';

const limiter = new Limiter(redis, { key: 'stripe-api', maxConcurrent: 5 });

// At most 5 of these are running at once. Across every process.
await limiter.run(() => stripe.charges.create(params));

Works with ioredis and node-redis v4+, survives Redis Cluster, survives a crashed holder, and keeps no configuration in Redis to lose.

npm install conclimit

Why this exists

bottleneck does this and is the only widely-used option — 10.5M downloads a week. Its last commit was July 2020. Its three most-reacted open issues:

| | | |---|---| | ERR SETTINGS_KEY_NOT_FOUND in clustering mode | 14 reactions, open since 2022 | | "Is bottleneck still maintained?" | 13 reactions | | "Support for node-redis v4" | 12 reactions, zero replies |

A commenter on the first one asks: "Do we have any workaround to resolve this issue? Or do we have any alternate library?"

This is that. Not a fork — the two failure modes above are design consequences, and the fixes are in the design.

The two things that were wrong, and how this differs

No settings live in Redis. SETTINGS_KEY_NOT_FOUND happens because bottleneck stores a limiter's configuration in a Redis key with a TTL, and when that key expires under a running limiter the Lua script has nothing to read and throws. Here every parameter — concurrency, spacing, reservoir, lease — is passed in on every call. There is no key to lose, so losing it is survivable:

await redis.del(`{conclimit:my-limiter}:running`, `{conclimit:my-limiter}:meta`);
await limiter.tryAcquire();   // works; rebuilt from its arguments

That is a test, not a claim.

Every key of one limiter shares a hash slot. Redis Cluster rejects a script whose keys hash to different slots, which is the other half of why clustering mode is where the bugs are. The limiter's name is wrapped in {} so the tag forces one slot, and the test asserts the two keys agree on CLUSTER KEYSLOT.

A crashed process must not shrink the limit

A holder that dies never releases, so without an expiry its slot is gone forever and the limiter deadlocks at a capacity that drops every time something crashes. Slots are leases in a sorted set scored by expiry, and every acquire sweeps the expired ones first.

The obvious problem with a lease is the opposite one: a slow job losing its slot to the expiry meant to catch dead holders, and then two jobs running under a limit of one. run() heartbeats for as long as the function takes, so the lease only has to cover a stall rather than the slowest job you will ever have. Both directions are tested — the reclaim, and the long job that must not be reclaimed.

Everything happens in one script

sweep expired leases → refill reservoir → check tokens
                     → check concurrency → check spacing → take the slot

Split across round trips this would not be a limiter: two callers would both read "one slot free" and both take it. One EVALSHA per acquire, with a NOSCRIPT fallback for when Redis has been restarted or flushed.

When a slot is refused, the script says when to try again — the next token's arrival, or the earliest lease expiry — rather than leaving the caller to guess an interval and either hammer Redis or sleep through an opening. The wait is jittered so that replicas woken by the same expiry do not stampede.

What it does

new Limiter(redis, {
  key: 'stripe-api',        // processes sharing this name share the limit
  maxConcurrent: 5,         // at most 5 running at once, everywhere
  minTime: 200,             // at least 200ms between two starts, everywhere
  reservoir: 100,           // a token bucket…
  reservoirRefillAmount: 100,
  reservoirRefillInterval: 60_000,   // …refilling 100 every minute
  leaseMs: 30_000,          // how long a slot survives a dead holder
  acquireTimeoutMs: 60_000,
});
await limiter.run(fn);              // acquire, heartbeat, release, always
const wrapped = limiter.wrap(fn);   // same, as a function
const slot = await limiter.acquire();   // manual; you release it
const { granted, retryAfterMs, running, tokens } = await limiter.tryAcquire();

What it does not do

No priorities, no queue introspection, no groups. bottleneck has all three. This is a limiter, not a job queue — if you need to reorder work, persist it, or watch it, you want a queue, and BullMQ is a better answer than either of us.

It is not a drop-in replacement. The API is deliberately smaller. Migrating means changing call sites, and the honest reason to do it is the two bugs above rather than an easier path.

It does not survive Redis being down. There is no local fallback, because the only two options are to stop (an outage) or to allow the work through (the overload you installed a limiter to prevent). Choosing silently would be worse than either; the error is yours to catch.

Redis Cluster is designed for and hash-tag tested, but the suite runs against a single node. Adding a three-node cluster to CI is on the list, and until it is there the claim is "the keys share a slot", which is what is asserted — not "tested end to end on a cluster".

Tests

npm install
npm run redis:up
npm test          # 27 tests against a real Redis
npm run redis:down

The same suite runs against both ioredis and node-redis, because supporting a client in the adapter and testing only the other one is the same as not supporting it. One test takes a slot through ioredis and is refused it through node-redis, which is as close to two processes as a single test gets.

Built with Claude

Claude wrote most of this code. The design is mine, and so is the decision to build it: the choice came from download counts and issue reactions rather than from an empty name on npm — a niche nobody has entered is usually a niche nobody wants, and this one has 10.5 million weekly downloads asking for a maintainer.

Licence

MIT.