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

limitron

v0.1.0

Published

Distributed rate limiter with Token Bucket, Fixed Window, and Sliding Window algorithms. Redis and in-memory backends. Express, Fastify, and Hono middleware included.

Readme

limitron

Distributed rate limiter for Node.js with three algorithms, two storage backends, and middleware for Express, Fastify, and Hono.

  • Algorithms — Sliding Window (default), Token Bucket, Fixed Window
  • Backends — Redis (distributed, production) · Memory (single-process, dev/test)
  • Middleware — drop-in middleware for Express, Fastify, Hono
  • TypeScript-first — full type definitions included, zero @types/ needed
  • Atomic — Redis operations use Lua scripts; no race conditions under concurrency

Installation

npm install limitron

For Redis-backed (distributed) usage, also install ioredis:

npm install ioredis

Quick Start

import { Limitron, MemoryAdapter } from 'limitron';

const limiter = new Limitron({
  adapter:   new MemoryAdapter(),   // or new RedisAdapter(redis)
  algorithm: 'sliding-window',      // default — most accurate
  limit:     100,                   // max requests
  window:    '1m',                  // per minute
});

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

if (!result.allowed) {
  console.log(`Rate limited. Retry in ${result.retryAfter}ms`);
}

Algorithms

Sliding Window (recommended)

Maintains a rolling log of request timestamps. At any instant, only requests within the last window count against the limit. No boundary bursts.

const limiter = new Limitron({
  adapter:   new RedisAdapter(redis),
  algorithm: 'sliding-window',
  limit:     100,
  window:    '1m',
});

Best for: APIs that need strict, accurate rate limiting.


Token Bucket

A bucket refills at a steady rate up to limit tokens. Each request consumes one token. Allows controlled bursting when the bucket is full.

const limiter = new Limitron({
  adapter:   new RedisAdapter(redis),
  algorithm: 'token-bucket',
  limit:     100,     // bucket capacity
  window:    '1m',    // time to fill bucket from empty to full
});

Best for: APIs where occasional bursts are acceptable (e.g. batch upload endpoints).


Fixed Window

Divides time into fixed slots (aligned to the clock). Simple counter, resets at each boundary.

const limiter = new Limitron({
  adapter:   new RedisAdapter(redis),
  algorithm: 'fixed-window',
  limit:     100,
  window:    '1m',
});

Best for: Simple use cases where implementation simplicity matters more than burst precision.

Note: Fixed Window has a known boundary-burst issue — up to 2× the limit can pass in a short period at window boundaries. Use Sliding Window if this matters.


Window Format

The window option accepts a human-readable string or raw milliseconds:

| Format | Meaning | |--------|---------| | '500ms' | 500 milliseconds | | '30s' | 30 seconds | | '5m' | 5 minutes | | '2h' | 2 hours | | '1d' | 1 day | | 60000 | 60 seconds (raw ms) |


Storage Backends

MemoryAdapter

In-process storage. No dependencies. Great for single-process apps, local dev, and unit testing.

import { Limitron, MemoryAdapter } from 'limitron';

const limiter = new Limitron({
  adapter: new MemoryAdapter(),
  limit: 10,
  window: '1m',
});

Not suitable for multi-process or multi-machine deployments — each process has an independent counter.

RedisAdapter

Redis-backed, fully distributed. All operations use atomic Lua scripts — correct under any concurrency.

import { Limitron, RedisAdapter } from 'limitron';
import { Redis } from 'ioredis';

const redis  = new Redis({ host: 'localhost', port: 6379 });
const limiter = new Limitron({
  adapter: new RedisAdapter(redis),
  limit:   100,
  window:  '1m',
});

Requirements: Redis 2.8+, ioredis 5+.


API

new Limitron(config)

| Option | Type | Default | Description | |--------|------|---------|-------------| | adapter | StorageAdapter | required | MemoryAdapter or RedisAdapter | | limit | number | required | Max requests per window | | window | string \| number | required | Window duration ('1m', 30000, etc.) | | algorithm | 'sliding-window' \| 'token-bucket' \| 'fixed-window' | 'sliding-window' | Algorithm to use | | keyPrefix | string | 'limitron:sw:' etc. | Storage key prefix for namespacing |


limiter.check(identifier)

Consume one request slot for identifier. Returns a RateLimitResult.

const result = await limiter.check('user:123');
// or by IP:
const result = await limiter.check(`ip:${req.ip}`);

limiter.peek(identifier)

Read the current quota state without consuming a slot. Useful for status/quota endpoints.

const status = await limiter.peek('user:123');
console.log(`${status.remaining} requests remaining`);

limiter.reset(identifier)

Clear all rate-limit state for identifier. Next check() starts fresh.

// Admin action — unblock a user
await limiter.reset('user:123');

limiter.getHeaders(result)

Generate standard HTTP rate-limit response headers from a result object.

const result = await limiter.check('user:123');
res.set(limiter.getHeaders(result));
// Sets:
//   X-RateLimit-Limit:     100
//   X-RateLimit-Remaining: 42
//   X-RateLimit-Reset:     1741473600  (unix seconds)
//   Retry-After:           47          (only when denied)

RateLimitResult

Every check() and peek() call returns:

interface RateLimitResult {
  allowed:    boolean;  // whether the request is permitted
  limit:      number;   // configured max requests per window
  remaining:  number;   // requests left in the current window
  resetAt:    number;   // unix timestamp (ms) when window resets
  retryAfter: number;   // ms to wait before retrying (0 if allowed)
}

Middleware

Express

import express from 'express';
import { Limitron, RedisAdapter } from 'limitron';
import { createExpressMiddleware } from 'limitron/middleware/express';
import { Redis } from 'ioredis';

const app    = express();
const limiter = new Limitron({ adapter: new RedisAdapter(new Redis()), limit: 100, window: '1m' });

// Apply to all /api routes
app.use('/api', createExpressMiddleware({ limiter }));

// Custom identifier (API key instead of IP)
app.use('/api', createExpressMiddleware({
  limiter,
  identifier: (req) => req.headers['x-api-key'] as string ?? req.ip ?? 'anon',
  onDenied:   (req, res) => res.status(429).json({ error: 'Rate limit exceeded' }),
}));

Fastify

import Fastify from 'fastify';
import { Limitron, RedisAdapter } from 'limitron';
import { createFastifyHook } from 'limitron/middleware/fastify';

const fastify = Fastify();
const limiter  = new Limitron({ adapter: new RedisAdapter(new Redis()), limit: 100, window: '1m' });

// Global rate limiting
fastify.addHook('onRequest', createFastifyHook({ limiter }));

// Route-level (stricter for login)
fastify.post('/auth/login', {
  onRequest: createFastifyHook({
    limiter:    new Limitron({ adapter: new RedisAdapter(new Redis()), limit: 5, window: '15m' }),
    identifier: (req) => req.headers['x-forwarded-for'] as string ?? req.ip,
  }),
  handler: async (req, reply) => { /* ... */ },
});

Hono

import { Hono } from 'hono';
import { Limitron, MemoryAdapter } from 'limitron';
import { createHonoMiddleware } from 'limitron/middleware/hono';

const app     = new Hono();
const limiter  = new Limitron({ adapter: new MemoryAdapter(), limit: 100, window: '1m' });

app.use('/api/*', createHonoMiddleware({ limiter }));

// Cloudflare Workers — use CF-Connecting-IP
app.use('*', createHonoMiddleware({
  limiter,
  identifier: (c) => c.req.header('cf-connecting-ip') ?? 'unknown',
}));

Use with HTTP headers in a request handler

app.get('/api/data', async (req, res) => {
  const result = await limiter.check(`user:${req.user.id}`);

  // Always set headers — clients use these to track quota proactively
  res.set(limiter.getHeaders(result));

  if (!result.allowed) {
    return res.status(429).json({ error: 'Too Many Requests' });
  }

  res.json({ data: '...' });
});

Multiple limiters

Use multiple Limitron instances for different endpoints or tiers:

const globalLimiter = new Limitron({ adapter, limit: 1000, window: '1m' });
const authLimiter   = new Limitron({ adapter, limit: 5,    window: '15m', keyPrefix: 'limitron:auth:' });
const apiLimiter    = new Limitron({ adapter, limit: 100,  window: '1m',  keyPrefix: 'limitron:api:' });

// Always use a unique keyPrefix when running multiple limiters on the same Redis

License

MIT