Build an IP Based Rate Limiting Module in Node.js with Typescript
I recently wrote a very simple IP address based rate limit module called rt-limit
to throttle requests for this blog. Although rate limiting is more commonly done on reverse proxies like Nginx and Apache, being able to manage traffic on your own back-end server is always more flexible.
And there are no fancy schemes in this rate limit module. Simply define a max
of request points that are allowed in a period
of time. When an IP address makes a request with certain points
, it consumes that amount from the record
table. If there are no more points left, then the request is rejected.
Build Your Own Rate Limiting Module
Follow this section to write your own rate limiting module in TypeScript, or to use the library directly.
Define Limit Record
Define a limit record to store initial time and request points left for each IP address:
interface Ratelimit_Record {
timestamp: number;
left: number;
}
TypeScript
Main Code
We'll be creating a class
for the module, so different rate limit instances could be created at runtime:
class Ratelimit {
private readonly max: number;
private readonly period: number;
// create storage for records
private records: Record<string, Ratelimit_Record> = {};
// this constructor takes two parameter `max` and `period`, setting the max points can be cosume in the `period` of time
constructor(max: number, period: number) {
this.max = max;
this.period = period;
}
}
TypeScript
Create a helper to initialize each record with the given timestamp
and max points this.max
:
class Ratelimit {
...
private initialize(ip: string, timestamp: number): Ratelimit_Record {
// add record to `this.records` and return it
return this.records[ip] = {
timestamp: timestamp,
left: this.max,
};
}
}
TypeScript
And a public
method to check if the IP address is exceeding the limit:
class Ratelimit {
...
public consume(ip: string, points = 1): boolean {
const timestamp = Date.now();
// check if record exists
let record = this.records[ip];
if (!record) {
// initialize record if not exits
record = this.initialize(ip, timestamp);
} else if (timestamp - record.timestamp >= this.period) {
// delete the record if it's expired
// the reason we're deleting the record instead of replacing it is to not disturb the insertion order of `records`
delete this.records[ip];
record = this.initialize(ip, timestamp);
}
// return `true` if points left is > 0 after consumed `points`
return record.left > 0 && (record.left -= points) > 0;
}
}
TypeScript
Add an interval
to the constructor to periodically clean up expired records:
export default class Ratelimit {
...
constructor(max: number, period: number) {
...
setInterval(() => {
const timestamp = Date.now();
for (const ip in this.records) {
if (timestamp - this.records[ip].timestamp < this.period) {
// since our `records` is ordered by insertion time, so stop the iteration after first non-expired record
break;
}
delete this.records[ip];
}
}, this.period);
}
...
}
TypeScript
Be aware the order of Javascript
for...in
iteration is not guaranteed to be insertion order in some browser. It is only safe to use in Node.js, and make sure the object's keys are not integers.
Usage
Create a rate limit instance with a maximum of 60
points in a 60 * 1000
millisecond period:
const ratelimit = new Ratelimit(60, 60 * 1000);
TypeScript
Test if 127.0.0.1
has exceeded the limit after consuming 2
points:
if (ratelimit('127.0.0.1', 2)) {
console.log(`permitted`);
} else {
console.log(`rate limit exceeded`);
}
TypeScript
Full Example
Module
ratelimit.tsexport interface Ratelimit_Record {
timestamp: number;
left: number;
}
export default class Ratelimit {
private readonly max: number;
private readonly period: number;
private records: Record<string, Ratelimit_Record> = {};
constructor(max: number, period: number) {
this.max = max;
this.period = period;
setInterval(() => {
const timestamp = Date.now();
for (const ip in this.records) {
if (timestamp - this.records[ip].timestamp < this.period) {
break;
}
delete this.records[ip];
}
}, this.period);
}
public consume(ip: string, points = 1): boolean {
const timestamp = Date.now();
let record = this.records[ip];
if (!record) {
record = this.initialize(ip, timestamp);
} else if (timestamp - record.timestamp >= this.period) {
delete this.records[ip];
record = this.initialize(ip, timestamp);
}
return record.left > 0 && (record.left -= points) > 0;
}
private initialize(ip: string, timestamp: number): Ratelimit_Record {
return this.records[ip] = {
timestamp: timestamp,
left: this.max,
};
}
}
TypeScript
App
index.tsimport Ratelimit from './ratelimit';
import express from 'express';
const ratelimit = new Ratelimit(60, 60 * 1000);
const app = express();
app.use((req, res, next) => {
if (ratelimit.consume(req.ip, 1)) {
next();
return;
}
res.status(429).end();
});
TypeScript
Use Directly
To use the rate limiting module without building your own, install rt-limit
from npm:
$ npm i rt-limit
Then import rt-limit
and it's ready to use with your server (e.g. express, koa):
import Ratelimit from 'rt-limit';
import express from 'express';
const ratelimit = new Ratelimit(60, 60 * 1000);
const app = express();
app.use((req, res, next) => {
if (ratelimit.consume(req.ip, 1)) {
next();
return;
}
res.status(429).end();
});
TypeScript