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.).
- 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
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-redisimport { 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`);
}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
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 | Fixed Window | Sliding Window |
|---|---|---|
| ioredis | ✅ | ✅ |
| iovalkey | ✅ | ✅ |
| node-redis | ✅ | ✅ |
| Bun Redis | ✅ | ❌ |
import { Ratelimit, type RedisClient, type RatelimitResponse } from 'io-ratelimiter';
const limiter = new Ratelimit(client: RedisClient, options: RatelimitOptions);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)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;
}await limiter.limit(identifier: string): Promise<RatelimitResponse>The identifier is typically a user ID, IP address, or API key.
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' });
}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();
});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();
});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)
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
Built by Kasper