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 proxy like Nginx and Apache, being able to manage traffic on your own back-end server is always more flexible.
And there is no fancy schemes in this rate limit module, simply define a max
of request points are allowed in a period
of time. When an IP address made a request with certain points
, it consumed from the record
table. If there is no more points left, then the request is rejected.

429
Build Your Own Rate Limiting Module
Follow this section to wrote 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
Create Class
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 the public
method to check if the IP address is exceeding 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 a 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 object's keys are not integer.
Usage
Create a rate limit instance with maximum of 60
points in the period of 60 * 1000
milliseconds:
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.ts
export 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.ts
import 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
Bash
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