io-ratelimiter
v1.0.0
Published
A flexible rate limiting library with support for fixed and sliding windows using Redis/Valkey
Downloads
98
Maintainers
Readme
io-ratelimiter
A flexible Redis-based rate limiting library supporting both fixed and sliding window algorithms. Works with any Redis client (ioredis, iovalkey, node-redis, Bun's native client, etc.).
Features
- Fixed window rate limiting
- Sliding window rate limiting with weighted scoring
- Redis/Valkey-backed for distributed systems
- Full TypeScript support
- High performance Lua script execution (EVALSHA)
- Protection against race conditions
- Client-agnostic - bring your own Redis client
- Zero runtime dependencies
Installation
bun add io-ratelimiter
# or
npm install io-ratelimiter
# or
yarn add io-ratelimiter
# or
pnpm add io-ratelimiterYou'll also need a Redis client:
# Choose one:
npm install ioredis
npm install iovalkey
npm install redis # node-redisQuick Start
import { Ratelimit } from 'io-ratelimiter';
import Redis from 'iovalkey'; // or 'ioredis' or 'redis'
// Create any Redis client
const client = new Redis('redis://localhost:6379');
// Create rate limiter (10 requests per 60 seconds)
const limiter = new Ratelimit(
client,
Ratelimit.slidingWindow({
limit: 10,
window: 60, // seconds
prefix: 'my-api', // optional
}),
);
// Check rate limit
const result = await limiter.limit('user-123');
if (result.success) {
// Process request
console.log(`${result.remaining} requests remaining`);
} else {
// Rate limit exceeded
console.log(`Try again in ${result.retry_after}ms`);
}Rate Limiting Algorithms
Fixed Window
Divides time into fixed intervals (e.g., 60-second windows) and tracks requests within each window. Simple and fast.
const limiter = new Ratelimit(
client,
Ratelimit.fixedWindow({
limit: 100,
window: 60,
prefix: 'api',
}),
);Best for: High-throughput scenarios where slight bursts are acceptable
Sliding Window
Provides smoother rate limiting by considering both current and previous windows with weighted rates. More accurate but requires Lua script support.
const limiter = new Ratelimit(
client,
Ratelimit.slidingWindow({
limit: 100,
window: 60,
prefix: 'api',
}),
);Best for: Preventing burst traffic, fair rate limiting
Note: Requires script() and evalsha() support. Works with ioredis, iovalkey, and node-redis. Bun's native Redis client currently only supports fixed window.
Client Compatibility
| Client | Fixed Window | Sliding Window | | ------------------------------------------------- | ------------ | -------------- | | ioredis | ✅ | ✅ | | iovalkey | ✅ | ✅ | | node-redis | ✅ | ✅ | | Bun Redis | ✅ | ❌ |
API Reference
Ratelimit Class
import { Ratelimit, type RedisClient, type RatelimitResponse } from 'io-ratelimiter';
const limiter = new Ratelimit(client: RedisClient, options: RatelimitOptions);Configuration Options
interface RatelimitOptionsWithoutType {
/** Maximum requests per window */
limit: number;
/** Window duration in seconds */
window: number;
/** Optional Redis key prefix */
prefix?: string;
}
// Factory methods
Ratelimit.fixedWindow(options: RatelimitOptionsWithoutType)
Ratelimit.slidingWindow(options: RatelimitOptionsWithoutType)Response Type
interface RatelimitResponse {
/** Whether the request is allowed */
success: boolean;
/** Maximum number of requests allowed in the window */
limit: number;
/** Number of remaining requests in current window */
remaining: number;
/** Time in milliseconds until the next request will be allowed (0 if under limit) */
retry_after: number;
/** Time in milliseconds when the current window expires completely */
reset: number;
}limit() Method
await limiter.limit(identifier: string): Promise<RatelimitResponse>The identifier is typically a user ID, IP address, or API key.
Framework Integration Examples
Next.js App Router
import { Ratelimit } from 'io-ratelimiter';
import { NextResponse } from 'next/server';
import { headers } from 'next/headers';
import Redis from 'iovalkey';
const client = new Redis(process.env.REDIS_URL);
const ratelimit = new Ratelimit(
client,
Ratelimit.slidingWindow({ limit: 10, window: 60 }),
);
export async function GET() {
const headersList = await headers();
const ip = headersList.get('x-forwarded-for') || '127.0.0.1';
const { success, remaining, reset, retry_after } =
await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{
status: 429,
headers: {
'X-RateLimit-Limit': '10',
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
'Retry-After': Math.ceil(retry_after / 1000).toString(),
},
},
);
}
return NextResponse.json({ message: 'Success' });
}Express Middleware
import { Ratelimit } from 'io-ratelimiter';
import express from 'express';
import Redis from 'iovalkey';
const app = express();
const client = new Redis(process.env.REDIS_URL);
const ratelimit = new Ratelimit(
client,
Ratelimit.slidingWindow({ limit: 100, window: 60 }),
);
app.use(async (req, res, next) => {
const { success, remaining, reset, retry_after } = await ratelimit.limit(
req.ip,
);
res.setHeader('X-RateLimit-Limit', '100');
res.setHeader('X-RateLimit-Remaining', remaining.toString());
res.setHeader('X-RateLimit-Reset', reset.toString());
if (!success) {
res.setHeader('Retry-After', Math.ceil(retry_after / 1000).toString());
return res.status(429).json({ error: 'Too many requests' });
}
next();
});Hono
import { Ratelimit } from 'io-ratelimiter';
import { Hono } from 'hono';
import Redis from 'iovalkey';
const app = new Hono();
const client = new Redis(process.env.REDIS_URL);
const ratelimit = new Ratelimit(
client,
Ratelimit.fixedWindow({ limit: 50, window: 60 }),
);
app.use('*', async (c, next) => {
const ip = c.req.header('x-forwarded-for') || 'unknown';
const { success, remaining, reset, retry_after } =
await ratelimit.limit(ip);
c.header('X-RateLimit-Limit', '50');
c.header('X-RateLimit-Remaining', remaining.toString());
if (!success) {
return c.json({ error: 'Too many requests' }, 429);
}
await next();
});Performance
The sliding window algorithm uses SCRIPT LOAD + EVALSHA for optimal performance:
- Script is loaded once on first use
- Subsequent requests send only a 40-byte SHA hash instead of the full ~800-byte Lua script
- Atomic operations prevent race conditions
- Automatic retry on script loss (e.g., Redis restart)
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
License
Credits
Built by Kasper
