Implementing Api rate limiting using redis and nodejs

Implementing Api rate limiting using redis and nodejs

Introduction

Rate limiting is a must have functionality in an Api for various reasons. Let us explore how do we implement it using redis and nodejs in simple steps. Before we go into the implementation lets us understand rate limiting in brief.

What is rate limiting ?

As the name suggests it is limiting the rate of usage of an API. It is a technique to control the rate or frequency at which a client can make requests to an API, Web Services or a similar application which requires client interaction.

It is commonly used is APIs for the following reasons -

  1. Prevent Abuse - An API exists to serve the requests from the client and provide required resources. However the clients can overload the server resources by making a huge number of request within a short time frame. This would lead to denial of service to the other clients requesting for the resource.

  2. Protect Server Resources - A huge number of requests within a short time frame would consume more bandwidth of the network , server or database. This would lead to an increase in the billing of server resources and also exhausting the limit of the server resources.

  3. Ensure fair usage - Rate limiting ensures that every client gets an equal share of the service usage limit and no single client can take the control of the service leading others to not able to use the service.

  4. Maintain System Stability - Rate limiting ensures system stability by preventing overoad, graceful handling of traffic, conservation of resources and controlling the cost.

Important aspects of implementing rate limiting for an API

When implementing rate limiting for an API we need to consider few things so that we can leverage the benefits of rate limiting to the fullest.

  1. Data Store - Since we are going to keep track of the number of requests made by each client we would need a data store to persist this information. Now the ideal candidate can be an in memory data store since the data of number of requests made by each clients would not be needed in future typically and it can be flused after a certian period of time. Redis is a very popular in memory data store and we would see how we use the redis data store to implement rate limiting later in the post.

  2. Rate limit - We need to define the rate of requests for an API. It would typically be in Rate per minute or Rate per second. The exact limit would depend on the API you are implementing. For example - In an application the rate for a login end point can be different from the rate for an API fetching the masters data. We can keep the rate limit for all the endpoints same or make it adjustible for different endpoints. It completely depends on the type of application you are building but keeping the rate flexible is usually recommended.

  3. Parameters for rate limiting - This is another important aspect while implemting rate limiting. We can have rate limits set up on client IP or specific to a token or either of IP or Token and endpoint.

  4. Algorithms - The algorithms define the stragety of the rate limiting. We can allow a certain number of request in a minute or a certain number of requests in the last 10 mins an so on. Some of the popular algorithms are - Fixed Window Counter

    Sliding Window Counter

    Token Bucket

    Leaky Bucket

    Distributed Rate Limiting

    Adaptive Rate Limiting

    Time-Based Window

    Concurrency Based Rate

  5. Expiry Time - Expiry time is set to clear the in memory data and resets or refills the counter of the allowed number of requests for each client.

  6. Response Handling - We need to be specific while sending the response indicating the client that they have exhausted the allowed number of request they can make in a certain duration. We should set the HTTP Response StatusCode to 429 which indicates the client with "Too Many Requests".

  7. Middleware Integration - The rate limiting stragety should be implemented as a middleware in the APIs. The middleware will check the limits in the data store and response back with 429 HTTP StatusCode or forward the request to the next middleware for processing the request.

Creating an API in nodejs

Create a new folder of your choice and navigate inside the folder. For our example we are creating a folder named nodejs-rate-limiting

Initialize an npm repository inside the folder and install all the dependencies needed to create a API.

npm init 
npm install express nodemon redis

Create a script to run the nodejs app in the scripts section of package.json.

{
  "name": "nodejs-rate-limiting",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon server.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.18.2",
    "nodemon": "^3.0.1",
    "redis": "^4.6.8"
  }
}

Add a new file server.js and add the code as mentioned below.

const http = require('http')
const express = require('express')
const app = express()
const port = process.env.PORT || 3000
const server = http.createServer(app)

app.use('/api/1',(req,res,next)=>{
    res.status(200).json({message:"Response from api 1"})
})
app.use('/api/2', (req, res, next) => {
    res.status(200).json({ message: "Response from api 2" })
})
app.use('/api/3', (req, res, next) => {
    res.status(200).json({ message: "Response from api 3" })
})
server.listen(port, () => console.log(`Server listening on port 3000`))

The above code does the following -

Creates an express app Create 3 endpoints as per the path mentioned below -

http://localhost:3000/api/1
http://localhost:3000/api/2
http://localhost:3000/api/3

Creating the rate limiting middleware to the nodejs API

In the root of your API create a folder to contain all the middleware of your app.

Now create a new file named rate-limiter.js inside the middleware folder and add the below mentioned code.

const redis = require('redis')
const client = redis.createClient({
    url: process.env.REDIS_URL
})

const rateLimiter = (allowedRequests, time) => {
    return async (req, res, next)=> {
            try {
                //You can define your logic to determine the IP
                const ip = (req.headers['x-forwarded-for'] || req.connection.remoteAddress).split(',')[0]
                if (!client.isOpen) {
                    await client.connect();
                }
                let ttl
                const requests = await client.incr(ip)
                if (requests === 1) {
                    await client.expire(ip, time)
                    ttl = time
                } else {
                    ttl = await client.ttl(ip)
                }
                if (requests > allowedRequests) {
                    return res.status(429).json({
                        error: 'Too many requests. Try again after sometime. TTL' + ttl
                    });
                }
                next();
            } catch (err) {
                return res.status(500).json({
                    error: 'An error occurred while processing the request!' + err
                });
            }
        }
    }

    module.exports =rateLimiter

In the code above we are -

Creating a redis client

Creating a client of the redis package. You can create redis client in many different ways. You can either pass a URL to the createClient method or the domain,password,port etc... Its always good to refer to the official documentation for the same.

If you do not pass any parameter to the createClient method it will by default try to connect to the local redis server. Redis can be installed in a linux distribution how ever you can also install and get the redis server up and running by following the steps in the offical documentation.

https://redis.io/docs/getting-started/installation/install-redis-on-windows/

Adding the rate limiting logic

After we have created the redis client we then write the logic of our rateLimiter function. Our rateLimiter function takes 2 parameters -

allowedRequests - The total number of allowed requests

time - The time duration in which the requests defined in parameter 1 is allowed

The rateLimiter function in turn returns a function which received the request , response and next function from our nodejs express application.

In the example code above we are doing the rate limiting based on the IP of the client but as discussed previously rate limiting can be implemented on a various strategies.

We then get the IP of the client which is used as a key of the redis data used to limit the client. We then check if the connection to redis is open or not. We create a new connection is the connection is not open.

if (!client.isOpen) 
{
  await client.connect();
}

We then get the count of the requests made with the IP using the redis incr() method. If it is the first request from the client we set the expiry of the data as the time duration of the rateLimiter.

                let ttl
                const requests = await client.incr(ip)
                if (requests === 1) {
                    await client.expire(ip, time)
                    ttl = time
                } else {
                    ttl = await client.ttl(ip)
                }

We are also fetching the TTL (Time To Live) for the redis data against the key(IP) of the client. This information would be used to convey the user the time after which they can try issuing the request again to be processed successfully.

If the total number of requests made by the client exceeds the allowed value we respond back with HTTP StatusCode 429.

                if (requests > allowedRequests) {
                    return res.status(429).json({
                        error: 'Too many requests. Try again after sometime. TTL' + ttl
                    });
                }

If the count of the requests made is less than the allowed value we call the next() function invoking the next middleware or the processing logic of the api.

The rate limter function above has been applied to all the endpoints of the application however you can apply this to specific routes as per your need. You can also set different limits for different enpoints.

The code below applies the rateLimiter to APIs with different limits.

app.use('/api/1',rateLimiter(20,60) (req, res, next) => {
    res.status(200).json({ message: "Response from api 1" })
})
app.use('/api/2',rateLimiter(30,60) (req, res, next) => {
    res.status(200).json({ message: "Response from api 2" })
})
app.use('/api/3',rateLimiter(2,30) (req, res, next) => {
    res.status(200).json({ message: "Response from api 2" })
})

Conclusion

In this post we understood the following -

  • What is rate timiting ?

  • Important aspects of implementing rate limiting for an API

  • Creating an API in nodejs

  • Creating the rate limiting middleware to the nodejs API

  • Creating a redis client

  • Adding the rate limiting logic

You can refer to the complete source code of this post from -

https://github.com/abyshekhar/nodejs-rate-limiting

Thank you for reading the post and see you in the next post.

Buy a coffee for sudshekhar