We will be implementing a fixed window ratelimiter using typescript and redis.
To get started, clone the template using the command.
npx degit "nivekithan/ratelimit#template" ratelimit
This will setup typescript (with esbuild) and install all dependencies we will need for this project.
Once you have cloned the project you can run
pnpm i # To install all dependencies
pnpm run dev
If everything is working, it output hello world
.
What is fixed window Ratelimiter ?
In fixed window ratelimiter we will group fixed time intervals into one window and enforce number of requests allowed per window.
For example, if the time interval is 1 min
then we can group time from 00:00:00
to 00:01:00
to window 1
and group time from 00:01:00
to 00:02:00
to window 2
.
Code
Create a new file src/fixedWindow.ts
and create a class
to store redis
connection, window
, limit
// ./src/fixedWindow.ts
import { Redis } from "ioredis";
export class FixedWindowRatelimiter {
#db: Redis;
#window: number;
#limit: number;
constructor(redis: Redis, window: number, limit: number) {
this.#db = redis;
this.#window = window;
this.#limit = limit;
}
}
#db
- It stores the connection toredis
server#window
- It represents the time interval for each window. For example in10 requests per 1 second
, the window is1 second
.#limit
- Total number of requests allowed per window. For example in10 requests per 1 second
, the limit is10
.
Ratelimit indentifier
In most cases application ratelimit against per identifier
. This identifier
could be ip address
or userId
or geographic location
. That means statement 10 requests per 1 second
usually means 10 requests per 1 second per user
or 10 requests per 1 second per ip address
.
Generating redis key
redis
is key-value
store. We will be storing number of requests as value
and we can have windowId
(unique id representing a window) as key
. But since our algorithm will also support ratelimit identifier
the key should be a combination of windowId
and identifier
.
Create a new private function #getKey
#getKey(unqiueId: string) {
const windowId = Math.floor(Date.now() / (this.#window * 1_000));
const redisKey = `${windowId}:${unqiueId}`;
return redisKey;
}
The uniqueId
is same as identifier
.
We generate
windowId
usingMath.floor(Date.now() / (this.#window * 1_000));
Then combine both
windowId
anduniqueId
to create redis key which can be used to store number of requests
Performing ratelimit check
Create new function check
with
async check(uniqueId: string) {
const redisKey = this.#getKey(uniqueId);
const [[incrError, incrRes]] = (await this.#db
.multi()
.incr(redisKey)
.expire(redisKey, this.#window, "NX")
.exec())!;
if (incrError) {
throw incrError;
}
const totalRequest = incrRes as number;
const isRatelimitReached = totalRequest > this.#limit;
return { isRatelimitReached, totalRequest };
}
We will be generating
redis key
using the previously defined function#getKey
Redis supports command
multi
which allows you to apply multiple redis commands atomically.We will be using
incr
command to increase the number of requests stored inredisKey
by 1. If the key is new, thenredis
will set the value ofredisKey
to 0 and then applyincr
command.These
redisKeys
will not be used once the window has passed. Thus storing these keys forever only increases the storage of redis without any advantage. Therefore we will set an expiration time for these keys and once the time has passed.redis
remove those keys automatically for us.This can be done by using
expire
command, we set the expiration to bewindow
and set the option toNX
.Setting option
NX
means the redis will set the expiration time to the key only if there is no previous expiration time on that key.By checking the returned response from
incr
command we can know the total number of requests and by comparing it with thelimit
we can choose whether to accept the request or drop the request.
Testing
To test the ratelimiter, initalize it on src/index.ts
file
import { Redis } from "ioredis";
import { FixedWindowRatelimiter } from "./fixedWindow";
const redis = new Redis(); // Connect to your redis instance
const fixedRatelimiter = new FixedWindowRatelimiter(redis, 60, 3); // 3 requests per 1 minute
async function main() {
const { isRatelimitReached, totalRequest } = await fixedRatelimiter.check("1");
console.log({ isRatelimitReached, totalRequest });
process.exit(0);
}
main();
Make sure you have passed the correct connection information into
new Redis()
call and verify it has connected to the redis.
pnpm run dev
Then run this command multiple times to test the fixed window ratelimter.