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

@p-vbordei/token-bucket

v0.2.1

Published

Token-bucket rate limiter with continuous refill, async waiting, and a keyed registry (per-IP, per-user, ...). Zero dependencies.

Readme

token-bucket

ci

npm downloads bundle

A correct, dependency-free token-bucket rate limiter. Single bucket or a keyed registry (per-IP, per-user, ...). Continuous refill, sync or async wait. Pluggable clock for tests.

import { TokenBucket, TokenBucketRegistry } from "@p-vbordei/token-bucket";

// Single bucket: 100-burst capacity, 10 tokens/sec steady-state
const b = new TokenBucket({ capacity: 100, refillPerSecond: 10 });

if (b.tryTake()) {
  // allowed
}

await b.take(1, /* timeoutMs */ 5000);  // waits until a token is free

// Per-key buckets (one bucket per IP, with idle eviction)
const reg = new TokenBucketRegistry({
  capacity: 100,
  refillPerSecond: 10,
  evictAfterMs: 10 * 60_000,
});

if (reg.tryTake(req.ip)) {
  // allowed
}

Install

npm install @p-vbordei/token-bucket

Works with Node 20+, browsers, Bun, Deno. ESM + CJS.

Why

Token bucket is the standard rate-limiting algorithm:

  • A bucket holds up to capacity tokens
  • Refills at refillPerSecond tokens/sec continuously
  • Each request consumes one (or more) tokens
  • Empty bucket → reject or wait

It naturally handles burstiness (the bucket can fill up to capacity, allowing a burst) and steady-state limits (long-term rate ≤ refillPerSecond).

Most rate-limit libraries are coupled to a framework or to Redis. This is a primitive — works the same in Express, Fastify, Hono, Cloudflare Workers, or as a client-side throttle.

Recipes

Express-style middleware (per-IP)

import { TokenBucketRegistry } from "@p-vbordei/token-bucket";

const reg = new TokenBucketRegistry({
  capacity: 60,           // burst of 60
  refillPerSecond: 1,     // 1 req/sec sustained
  evictAfterMs: 60 * 60_000,  // forget idle IPs after 1h
});

app.use((req, res, next) => {
  if (!reg.tryTake(req.ip)) {
    res.setHeader("Retry-After", "1");
    return res.status(429).send("Too Many Requests");
  }
  next();
});

Client-side throttle for outbound API calls

import { TokenBucket } from "@p-vbordei/token-bucket";

// "50 requests per minute" → ~0.83/sec, allow burst of 10
const limiter = new TokenBucket({ capacity: 10, refillPerSecond: 50 / 60 });

async function callExternalApi() {
  await limiter.take();  // blocks until a token is free
  return await fetch(EXTERNAL);
}

Per-user quota

import { TokenBucketRegistry } from "@p-vbordei/token-bucket";

const userLimits = new TokenBucketRegistry({
  capacity: 1000,         // 1000-token daily quota
  refillPerSecond: 1000 / 86400,  // refills over 24h
  evictAfterMs: 30 * 86_400_000,  // forget users idle > 30 days
});

if (!userLimits.tryTake(userId, expensiveCost)) {
  throw new Error("daily quota exceeded");
}

"How long until I can?" hint

import { TokenBucket } from "@p-vbordei/token-bucket";

const b = new TokenBucket({ capacity: 10, refillPerSecond: 1 });

if (b.tryTake()) {
  doWork();
} else {
  const waitMs = b.msUntilAvailable();
  console.log(`Try again in ${waitMs}ms`);
}

Inject clock for tests

import { TokenBucket } from "@p-vbordei/token-bucket";

class FakeClock {
  private t = 0;
  now = () => this.t;
  advance(ms: number) { this.t += ms; }
}

const clock = new FakeClock();
const b = new TokenBucket({ capacity: 5, refillPerSecond: 1, now: clock.now });
b.tryTake(5);
clock.advance(3000);
expect(b.peek()).toBe(3);

API

new TokenBucket(config)

| Field | Type | Default | Meaning | |---|---|---|---| | capacity | number | required | Max tokens (burst size) | | refillPerSecond | number | required | Steady-state rate | | initialTokens | number | capacity | Tokens at construction | | now | () => number | Date.now | Injectable clock for tests |

Methods:

  • tryTake(n=1) → boolean — consume n tokens if available, else return false without consuming
  • take(n=1, timeoutMs?) → Promise<void> — wait until n tokens available, then consume; rejects if wait exceeds timeoutMs
  • msUntilAvailable(n=1) → number — ms to wait for n tokens; Infinity if impossible
  • peek() → number — current tokens (after applying refill)
  • reset() → void — restore to full capacity

new TokenBucketRegistry(config)

Extends TokenBucket config with evictAfterMs?: number. Buckets are created lazily per key (any value, hashed via Map).

Methods: tryTake(key, n?), take(key, n?, timeoutMs?), msUntilAvailable(key, n?), reset(key), clear(), size(), sweep().

sweep() is called automatically on every tryTake / take; you can also call it on a timer.

Notes

  • Token counts are floating-point so refill is exact at any rate (e.g. 0.1 tokens/sec is fine).
  • A bucket with refillPerSecond: 0 and empty tokens will never satisfy further requests; take() will reject immediately rather than block forever.
  • Both take() and tryTake() throw on n <= 0.

Caveats

  • In-memory only. For distributed rate limiting across instances, you need Redis or similar. The algorithm here is the right shape; use it inside your distributed implementation.
  • No backpressure on the producer. If you have a producer pushing faster than the bucket refills, take() queues forever. Bound the producer too.

License

Apache-2.0 © Vlad Bordei