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

ae-rate-limiter

v1.0.0

Published

A flexible, pluggable rate-limiting library supporting token bucket, fixed window, and sliding window algorithms with Redis or in-memory backends

Readme

rate-limiter

A flexible rate-limiting library for Node.js. Framework-agnostic core with optional Express middleware.

npm install rate_limiter

Table of Contents


Architecture Overview

┌─────────────────────────────────────────────────┐
│                  Your Code                       │
│  rateLimiter.execute("user:123")                 │
└──────────────┬──────────────────────────────────┘
               │
┌──────────────▼──────────────────────────────────┐
│               Algorithm                          │
│  (TokenBucket / WindowFixedCount / ...)          │
└──────────────┬──────────────────────────────────┘
               │
┌──────────────▼──────────────────────────────────┐
│              CacheClient                         │
│  (get / set / increment)                         │
└──────┬───────────────────────────┬──────────────┘
       │                           │
       ▼                           ▼
   RedisClient              MockCacheClient
   (ioredis)                (in-memory)

Three layers:

  1. Algorithm — the rate-limiting logic (what counts, when to refill, what to reject)
  2. CacheClient — storage abstraction (how counts are stored and retrieved)
  3. Backend — the actual database or in-memory store

Choosing Your Backend

Every constructor takes a CacheClient. There are two built-in implementations:

RedisClient (production)

import Redis from 'ioredis';
import { RedisClient, TokenBucket } from 'rate_limiter';

const redis = new Redis({ host: 'localhost', port: 6379 });
const cache = new RedisClient(redis);

const limiter = new TokenBucket(cache, 100, 10);

MockCacheClient (testing / development)

import { TokenBucket } from 'rate_limiter';
import MockCacheClient from './test/mock_cache_client.js';

const cache = new MockCacheClient();
const limiter = new TokenBucket(cache, 100, 10);

Custom backend

Implement the CacheClient interface:

import type { CacheClient } from 'rate_limiter';

class MyCache implements CacheClient {
  async get(key: string): Promise<string | null> { ... }
  async set(key: string, value: string | number): Promise<void> { ... }
  async increment(key: string): Promise<number> { ... }
}

Choosing an Algorithm

Token Bucket

Tokens refill at a steady rate; burst up to capacity. Good for API rate limits where short bursts should be allowed.

| Variant | Storage | Race-safe? | Constructor | |---|---|---|---| | TokenBucket | Any CacheClient | No | new TokenBucket(cache, limit, fillRate) | | TokenBucketLua | Redis only | Yes | new TokenBucketLua(redisClient, limit, fillRate) |

Parameters:

  • limit — bucket capacity (max tokens)
  • fillRate — tokens replenished per second

How it works: Each request increments a counter. When (now - lastRefill) >= (limit / fillRate) * 1000ms, the counter resets to 0. If the counter exceeds limit, the request is rejected.

Fixed Window

A simple counter that resets every N milliseconds. Simple and memory-efficient.

| Variant | Storage | Race-safe? | Constructor | |---|---|---|---| | WindowFixedCount | Any CacheClient | No | new WindowFixedCount(cache, limit, timeWindowMs) | | WindowFixedCountLua | Redis only | Yes | new WindowFixedCountLua(redisClient, limit, timeWindowMs) |

Parameters:

  • limit — max requests per window
  • timeWindowMs — window duration in milliseconds

How it works: A counter and a refill timestamp are stored per key. When the window expires, both reset to 0. The counter increments until it hits the limit.

Sliding Window Log

Tracks individual request timestamps. Provides the most accurate windowing but uses more memory per key.

| Variant | Storage | Race-safe? | Constructor | |---|---|---|---| | SlidingWindowLog | Any CacheClient | No | new SlidingWindowLog(cache, limit, windowMs) | | SlidingWindowLogLua | Redis only | Yes | new SlidingWindowLogLua(redisClient, limit, windowMs) |

Parameters:

  • limit — max requests per sliding window
  • windowMs — window duration in milliseconds

How it works (non-Lua): Stores a JSON array of timestamps per key. On each request, entries older than now - windowMs are removed, the current timestamp is appended, and if the array length exceeds limit the request is rejected. The request is always logged even if rejected (strict logging).

How it works (Lua): Uses a Redis sorted set (ZSET). Timestamps are scores, unique request IDs are members. Old entries are removed via ZREMRANGEBYSCORE. The count is obtained via ZCARD.


Lua vs Non-Lua Variants

Every algorithm has two variants:

| Variant | Suffix | Requires Redis | Atomic | Use case | |---|---|---|---|---| | Non-Lua | (no suffix) | No | No | Testing, single-server, low-concurrency | | Lua | *Lua | Yes | Yes | Production, multi-server, high-concurrency |

Why use Lua?

The non-Lua variants make separate calls for get, set, and increment. Between these calls, another process or request can interleave, causing a race condition:

Request A: get(key) → 99
Request B: get(key) → 99
Request A: 99 >= 100? No → set(key, 100)
Request B: 99 >= 100? No → set(key, 100) ← BOTH PASSED, limit exceeded!

Lua scripts run atomically inside Redis — no other commands execute while the script runs. This guarantees correctness under concurrent load.

// Safe for production — atomic Redis Lua execution
import { TokenBucketLua } from 'rate_limiter';
const limiter = new TokenBucketLua(redisClient, 100, 10);

The *Lua classes take RedisClient (not CacheClient) because they call eval() to run scripts. The non-Lua classes take CacheClient and work with any backend.


Usage Without Express

The core library has zero framework dependencies — just call execute(key).

Basic rate limiting

import { TokenBucket } from 'rate_limiter';
import MockCacheClient from './test/mock_cache_client.js';

const cache = new MockCacheClient();
const limiter = new TokenBucket(cache, 5, 1);  // 5 requests/sec

async function handleRequest(userId: string) {
  const allowed = await limiter.execute(userId);
  if (!allowed) {
    return { status: 429, body: 'Too many requests' };
  }
  return { status: 200, body: 'OK' };
}

With Redis in production

import Redis from 'ioredis';
import { RedisClient, SlidingWindowLogLua } from 'rate_limiter';

const redis = new Redis();
const cache = new RedisClient(redis);
const limiter = new SlidingWindowLogLua(cache, 100, 60_000);

const allowed = await limiter.execute('api-key-abc123');

In a Fastify route

import Fastify from 'fastify';
import { TokenBucketLua, RedisClient } from 'rate_limiter';
import Redis from 'ioredis';

const app = Fastify();
const limiter = new TokenBucketLua(
  new RedisClient(new Redis()),
  100, 10
);

app.get('/api', async (req, reply) => {
  const key = req.ip ?? 'unknown';
  const allowed = await limiter.execute(key);
  if (!allowed) return reply.status(429).send('rate limited');
  return { ok: true };
});

In a plain HTTP server

import { createServer, IncomingMessage, ServerResponse } from 'http';
import { TokenBucket, RedisClient } from 'rate_limiter';
import Redis from 'ioredis';

const limiter = new TokenBucket(new RedisClient(new Redis()), 100, 10);

createServer(async (req: IncomingMessage, res: ServerResponse) => {
  const key = req.socket.remoteAddress ?? 'unknown';
  const allowed = await limiter.execute(key);
  if (!allowed) {
    res.writeHead(429);
    return res.end('Too many requests');
  }
  res.writeHead(200);
  res.end('OK');
}).listen(3000);

Usage With Express

Simple setup (TokenBucket, IP-based key)

import express from 'express';
import Redis from 'ioredis';
import { RedisClient, createRateLimiter } from 'rate_limiter';

const app = express();
const cache = new RedisClient(new Redis());

app.use('/api', createRateLimiter({
  limit: 100,
  fillRate: 10,
  cacheClient: cache,
}));

Custom algorithm (use Lua for production)

import { TokenBucketLua } from 'rate_limiter';

app.use('/api', createRateLimiter({
  limit: 100,
  fillRate: 10,
  cacheClient: cache,
  algorithm: TokenBucketLua,   // ← atomic Lua variant
}));

Custom key extractor (rate-limit by API key)

app.use('/api', createRateLimiter({
  limit: 1000,
  fillRate: 50,
  cacheClient: cache,
  keyExtractor: (req) => req.headers['x-api-key'] as string ?? 'unknown',
}));

Full example with sliding window

import { SlidingWindowLogLua, createRateLimiter } from 'rate_limiter';

app.use('/api', createRateLimiter({
  limit: 60,
  fillRate: 60,                  // ignored by SlidingWindowLog
  cacheClient: cache,
  algorithm: SlidingWindowLogLua,
}));

Note: fillRate is unused by SlidingWindowLog* and WindowFixedCount* — they only need limit and the time window is passed at construction. But createRateLimiter always passes limit and fillRate to the algorithm constructor. For custom algorithms with different signatures, set up the Controller directly instead.

Using Controller directly for custom constructors

If your algorithm has a different signature than TokenBucket(cache, limit, fillRate), use Controller directly:

import { Controller, SlidingWindowLogLua, RedisClient } from 'rate_limiter';

const algorithm = new SlidingWindowLogLua(redisClient, 60, 60_000);
const controller = new Controller(algorithm, (req) => req.ip ?? 'unknown');

app.get('/api', (req, res, next) => controller.rateLimit(req, res, next));

CacheClient Interface

interface CacheClient {
  get(key: string): Promise<string | null>;
  set(key: string, value: string | number): Promise<void>;
  increment(key: string): Promise<number>;
}

get returns string | null (Redis returns strings; numbers are stored and retrieved as strings by Redis). Consumers should Number() the result when needed.

RedisClient

import { RedisClient } from 'rate_limiter';
import Redis from 'ioredis';

const client = new RedisClient(new Redis());

Also exposes eval(script, keys, argv) for running Lua scripts. Used by the *Lua algorithm classes.

MockCacheClient (in-memory for tests)

import MockCacheClient from './test/mock_cache_client.js';

const cache = new MockCacheClient();

Stores values in a Map<string, string>. No external dependencies needed.


API Reference

Algorithm (abstract class)

abstract class Algorithm {
  constructor(protected cacheClient: CacheClient);
  abstract execute(key: string): Promise<boolean>;
  abstract getLimit(): number;
}

| Method | Returns | Description | |---|---|---| | execute(key) | boolean | true = allowed, false = rate-limited | | getLimit() | number | The configured limit (used for response headers) |

Algorithm classes

| Class | Constructor signature | Requires Redis | |---|---|---| | TokenBucket | (cache: CacheClient, limit: number, fillRate: number) | No | | TokenBucketLua | (cache: RedisClient, limit: number, fillRate: number) | Yes | | WindowFixedCount | (cache: CacheClient, limit: number, timeWindow: number) | No | | WindowFixedCountLua | (cache: RedisClient, limit: number, timeWindow: number) | Yes | | SlidingWindowLog | (cache: CacheClient, limit: number, windowMs: number) | No | | SlidingWindowLogLua | (cache: RedisClient, limit: number, windowMs: number) | Yes |

createRateLimiter(options) — Express middleware factory

function createRateLimiter(options: RateLimiterOptions): RequestHandler;

| Option | Type | Default | Description | |---|---|---|---| | limit | number | required | Max requests allowed in the window | | fillRate | number | required | Refill rate (tokens/sec) — used by TokenBucket, ignored by others | | cacheClient | CacheClient | required | Storage backend | | algorithm | class constructor | TokenBucket | Algorithm class, e.g. TokenBucketLua, WindowFixedCountLua | | keyExtractor | (req) => string | req.ip ?? 'unknown' | Extracts the rate-limit key from the Express request |

Controller — low-level Express integration

class Controller {
  constructor(algorithm: Algorithm, keyExtractor?: (req: Request) => string);
  rateLimit(req: Request, res: Response, next: NextFunction): void;
}

Use when you need full control over the middleware lifecycle or when your algorithm has a non-standard constructor signature.


Running the Dev Server

# Requires Redis running on localhost:6379
npm run dev

Tests

npm test

Unit tests use MockCacheClient (in-memory). Integration tests use supertest against an Express app.

License

MIT