How to implement your own rate limiter?

NodeJS

Express

07/23/2021


Light weight, in-memory rate limiter using NodeJS

banner

Rate Limiting is one of the most important features which should be implemented in every web application. There are a lot of variations in the case of rate-limiting such as -

  • IP based rate limiting — preventing Denial of Service attacks

  • Account specific rate limiting — protection against brute force attacks

In this demo, we will implement a basic rate limiting mechanism which will lock the user’s account for X duration after Y consecutive invalid attempts. Also we are not going to use any database for this, as I want to keep it more simple and precise.

I’ll be using NodeJS to implement the rate limiter, but this logic can be implemented using any programming language.

The Process:

This is how it works!

Show me the code…

Create a new NodeJS project using the npm init --y command. We need to install express for handling the http requests and moment for displaying the relative time left for the user’s account to unlock.

BASH
npm install express moment --save

Once installed, we can add the following code to the app.js file.

JS
const express = require("express");
const moment = require("moment");
const app = express();
const PORT = process.env.PORT || 5000;
app.use(express.json());
const MAX_ATTEMPTS = 3; // after which the account should be locked
const LOCK_WINDOW = 2; // in minutes
let lock = {
attempts: 0,
isLocked: false,
unlocksAt: null,
};
let locks = {};
app.post("/login", async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.json({
error: true,
message: "Please enter email and password to continue",
status: 400,
});
}
if (
locks[email] &&
locks[email].isLocked &&
locks[email].unlocksAt > new Date()
)
return res.status(401).json({
error: true,
message:
"Account locked due to many invalid attempts. You account unlocks " +
moment(locks[email].unlocksAt).fromNow(),
});
// Not recommended ^^
const isValid = email == "test@gmail.com" && password == "complex_password";
//If the login attempt is invalid
if (!isValid) {
locks[email] = lock;
locks[email].attempts += 1;
if (locks[email].attempts >= MAX_ATTEMPTS) {
var d = new Date();
d.setMinutes(d.getMinutes() + LOCK_WINDOW);
locks[email].isLocked = true;
locks[email].unlocksAt = d;
}
return res.status(401).json({
error: true,
message:
"Sorry, please check whether you have entered the correct credentials.",
});
}
delete locks[email];
return res.send("Authentication success");
} catch (err) {
console.error("Login error", err);
return res.status(500).json({
error: true,
message:
"Sorry, couldn't process your request right now. Please try again later.",
});
}
});
// Returns all the locks -for testing purpose
app.get("/locks", async (req, res) => {
try {
return res.send(locks);
} catch (error) {
return res.status(500);
}
});
app.listen(PORT, () => console.log("Server started!"));

Let us save the file and run some tests using Postman!

We can see that in all the scenarios the rate limiter works as expected. As mentioned above, this is a very basic implementation and it can further be improved in many ways, for example we can define a time frame between which the number of hits can be restricted, say, 5 invalid login attempts within 3 minutes should lock the account for 24 hours. The locks are stored in the application’s memory and hence they will be lost if the server restarts or crashes. So the locks can be persisted in some data stores like Redis or MongoDB for more consistency and reliability. Also there are some npm modules for available to implement rate limiting.

Happy coding 🎉