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

@zipbul/rate-limiter

v0.2.3

Published

Framework-agnostic rate limiter engine with multiple algorithms and pluggable stores

Readme

@zipbul/rate-limiter

English | 한국어

npm coverage

A framework-agnostic rate limiter engine with multiple algorithms and pluggable stores.

Zero external runtime dependencies. Designed for Bun.

📦 Installation

bun add @zipbul/rate-limiter

🚀 Quick Start

import { RateLimiter, Algorithm, RateLimitAction } from '@zipbul/rate-limiter';

const limiter = RateLimiter.create({
  rules: { limit: 100, window: 60_000 },   // 100 requests per minute
  algorithm: Algorithm.SlidingWindow,
});

const result = await limiter.consume('user:123');

if (result.action === RateLimitAction.Allow) {
  // proceed
  console.log(result.remaining); // tokens left
} else {
  // throttled
  console.log(result.retryAfter); // ms until retry
}

🧮 Algorithms

Three built-in algorithms are available. All share the same API — just change algorithm.

| Algorithm | Best for | Behavior | |:----------|:---------|:---------| | SlidingWindow (default) | General API rate limiting | Weighted interpolation between current and previous window | | TokenBucket | Bursty traffic with steady refill | Continuous token refill at a fixed rate | | GCRA | Strict scheduling / cell rate control | Tracks Theoretical Arrival Time (TAT) per request |

// Token Bucket
RateLimiter.create({
  rules: { limit: 10, window: 1000 },
  algorithm: Algorithm.TokenBucket,
});

// GCRA
RateLimiter.create({
  rules: { limit: 10, window: 1000 },
  algorithm: Algorithm.GCRA,
});

⚙️ Options

interface RateLimiterOptions {
  rules: RateLimitRule | RateLimitRule[];  // Required
  algorithm?: Algorithm;       // Default: SlidingWindow
  store?: RateLimiterStore;    // Default: MemoryStore
  clock?: () => number;        // Default: Date.now
  cost?: number;               // Default: 1
  hooks?: RateLimiterHooks;
}

rules

One or more rate limit rules. When multiple rules are provided, all must pass (compound check).

// Single rule
RateLimiter.create({
  rules: { limit: 100, window: 60_000 },
});

// Compound rules: 10/s AND 100/min
RateLimiter.create({
  rules: [
    { limit: 10, window: 1000 },
    { limit: 100, window: 60_000 },
  ],
});

store

Pluggable storage backend. Defaults to an in-memory Map-based store.

import { MemoryStore } from '@zipbul/rate-limiter';

RateLimiter.create({
  rules: { limit: 100, window: 60_000 },
  store: new MemoryStore({ maxSize: 10_000, ttl: 120_000 }),
});

cost

Default number of tokens consumed per request. Can be overridden per call.

const limiter = RateLimiter.create({
  rules: { limit: 100, window: 60_000 },
  cost: 1,
});

// Expensive endpoint consumes 5 tokens
await limiter.consume('user:123', { cost: 5 });

hooks

Lifecycle callbacks for monitoring and logging.

RateLimiter.create({
  rules: { limit: 100, window: 60_000 },
  hooks: {
    onConsume: (key, result) => metrics.increment('rate_limit.allow'),
    onLimit: (key, result) => metrics.increment('rate_limit.deny'),
  },
});

📋 API

RateLimiter.create(options)

Creates a new rate limiter instance. Throws RateLimiterError on invalid options.

limiter.consume(key, options?)

Consumes tokens for the given key. Returns a discriminated union:

type RateLimitResult = RateLimitAllowResult | RateLimitDenyResult;

| Field | Allow | Deny | |:------|:------|:-----| | action | 'allow' | 'deny' | | remaining | Tokens left | 0 | | limit | Max tokens per window | Max tokens per window | | resetAt | Window reset timestamp (ms) | Window reset timestamp (ms) | | retryAfter | — | ms until next allowed request |

limiter.peek(key, options?)

Same as consume but does not modify state. Useful for checking limits without consuming tokens.

limiter.reset(key)

Removes all rate limit state for the given key.

💾 Stores

MemoryStore

Default in-memory store. Suitable for single-process deployments.

import { MemoryStore } from '@zipbul/rate-limiter';

new MemoryStore({
  maxSize: 10_000,   // FIFO eviction (default: unlimited)
  ttl: 120_000,      // Lazy TTL in ms (default: no expiry)
});

RedisStore

Distributed store using optimistic locking (CAS via Lua scripts).

import { RedisStore } from '@zipbul/rate-limiter';
import Redis from 'ioredis';

const redis = new Redis();
const store = new RedisStore({
  client: {
    eval: (script, keys, args) =>
      redis.eval(script, keys.length, ...keys, ...args),
  },
  prefix: 'rl:',      // Key prefix (default: 'rl:')
  ttl: 120_000,        // PEXPIRE in ms (default: no expiry)
  maxRetries: 5,       // CAS retry limit (default: 5)
});

RateLimiter.create({
  rules: { limit: 100, window: 60_000 },
  store,
});

withFallback

Wraps a primary store with automatic failover to a fallback store.

import { withFallback, MemoryStore } from '@zipbul/rate-limiter';

const store = withFallback(redisStore, new MemoryStore(), {
  healthCheck: async () => redis.ping() === 'PONG',
  restoreInterval: 30_000, // Health check interval (default: 30s)
});

// Don't forget to clean up when done
store.dispose();

🚨 Error Handling

RateLimiter.create() throws on invalid options. consume() wraps store failures as RateLimiterError.

import { RateLimiter, RateLimiterError, RateLimiterErrorReason } from '@zipbul/rate-limiter';

try {
  await limiter.consume('user:123');
} catch (e) {
  if (e instanceof RateLimiterError) {
    e.reason;  // RateLimiterErrorReason.StoreError
    e.message; // "Store operation failed"
    e.cause;   // Original error
  }
}

RateLimiterErrorReason

| Reason | Thrown by | Description | |:-------|:---------|:------------| | InvalidLimit | create() | limit must be a positive integer | | InvalidWindow | create() | window must be a positive integer (ms) | | InvalidCost | create() / consume() | cost must be a non-negative integer | | InvalidAlgorithm | create() | Unsupported algorithm value | | EmptyRules | create() | rules must not be empty | | StoreError | consume() / peek() | Store operation failed at runtime |

🔌 Custom Store

Implement the RateLimiterStore interface to use any backend:

import type { RateLimiterStore, StoreEntry } from '@zipbul/rate-limiter';

class MyStore implements RateLimiterStore {
  update(key: string, updater: (current: StoreEntry | null) => StoreEntry): StoreEntry | Promise<StoreEntry> { /* ... */ }
  get(key: string): StoreEntry | null | Promise<StoreEntry | null> { /* ... */ }
  delete(key: string): void | Promise<void> { /* ... */ }
  clear(): void | Promise<void> { /* ... */ }
}

📄 License

MIT