node-redis-token-bucket-ratelimiter
A rolling rate limit using Redis. Original idea from Peter Hayes.
Uses a lua script for atomic operations and to prevent blocked actions from substracting
from the bucket.
Compatible with ioredis (including in Redis Cluster mode) and node-redis client.
Usage
const RollingLimit = require('redis-token-bucket-ratelimiter');
const Redis = require('ioredis');
const redisClient = new Redis({port});
const myAppVersion = require('./package.json').version;
const defaultLimiter = new RollingLimit({
interval: 5000,
limit: 3,
redis: redisClient,
prefix: `${myAppVersion}:`,
force: false,
});
How It Works
Token Bucket ratelimiters can be described as a bucket within which "tokens" are added at a constant rate. Every time a request is made, a token is removed from the bucket. If the bucket is empty, the request is rejected.
For instance, one might set a 60/1min request limit by instantiating a limiter like so:
const requestLimiter = new RollingLimit({
interval: 60000,
limit: 60,
redis: RedisClient
});
Then use it as middleware on each request:
async function rateLimitMiddleware(req, res, next) {
const id = getUserId(req);
const limit = await requestLimiter.use(id);
res.set('X-RateLimit-Limit', String(limit.limit));
res.set('X-RateLimit-Remaining', String(limit.remaining));
const retrySec = Math.ceil(limit.retryDelta / 1000);
res.set(
'X-RateLimit-Reset',
String(Math.ceil(Date.now() / 1000) + retrySec)
);
if (limit.rejected) {
res.set('Retry-After', String(retrySec));
res.status(429).json({
error: {
message: `Rate limit exceeded, retry in ${retrySec} seconds.`,
name: 'RateLimitError',
},
});
return;
}
next();
}
RollingLimit Types and Methods
Types
type RollingLimiterOptions = {
interval: number,
limit: number,
redis: Object,
prefix?: string,
force?: boolean,
};
type RollingLimiterResult = {
limit: number,
remaining: number,
rejected: boolean,
retryDelta: number,
forced: boolean,
};
Methods
limiter = new RollingLimit(options: RollingLimiterOptions)
Creates a new RollingLimit instance. See types above.
limiter.use(id: string): Promise<RateLimitResponse>
limiter.use(id: string, amount?: number): Promise<RateLimitResponse>
Takes a token from the limit's bucket for id in redis and returns a promise with
a RollingLimiterResult object.
If you want to get the count of tokens left, send in an amount of 0.
static RateLimiter.stubLimit(max): RateLimitResponse
Synchronously returns a fake-but-complete response object, with the supplied max for a limit.