NoCache

Table of Contents

Build an IP Based Rate Limiting Module in Node.js with Typescript

Cyrus Kao
Last modified on .

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.

Status code 429
Requests exceeding the rate limit were rejected with code 429

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

See Also

Comments

Sign in to leave a comment.