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.
Maintainers
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 limitronFor Redis-backed (distributed) usage, also install ioredis:
npm install ioredisQuick 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 RedisLicense
MIT
