Published on

Rate Limit NestJS with Redis

1435 words8 min read6
Views
Authors
    Rate Limit NestJS with Redis

    In the previous article, we talked about how to make a khodam check website, there were a lot of impressions and responses from various groups who wanted to try it. Due to the large number of feature requests from users for a service that costs money, a rate limiting method is needed. It also method for busy traffic service such as reference in OpenAI Docs

    In today's world of high-traffic applications, it's critical to ensure that your API can handle the load while preventing abuse. Rate limiting is an important feature that helps protect your application by controlling the number of requests a user can make in a given period.

    In this blog post, we will discuss how to implement rate limiting in NestJS applications using Redis as the storage mechanism using Upstash SDK (because of course it's free *xd).

    Database Setup

    Create a Redis database using Upstash Console or Upstash CLI. Copy the UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN for the next steps.

    Installing Required Packages

    install the necessary packages for NestJS and Redis:

    npm install @nestjs/throttler @nestjs/redis ioredis nestjs-throttler-storage-redis

    Configure Client Redis Upstash

    configure upstash redis from your settings

    const UPSTASH_REDIS_TOKEN = process.env.UPSTASH_REDIS_TOKEN ?? ""

    const client = new Redis(`rediss://default:${UPSTASH_REDIS_TOKEN}@mature-duckling-37999.upstash.io:6379`);

    Integrating Throttler and Redis Storage into Module there option to global configure in app.module or specific module. in this. case it implemented in ai.module

    import { Module } from '@nestjs/common';

    import { AIService } from './ai.service';

    import { AIController } from './ai.controller';

    import { ThrottlerModule } from '@nestjs/throttler';

    import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis';

    import { Redis } from 'ioredis';

    @Module({ 

    imports: [

    ``ThrottlerModule.forRoot({

            throttlers: [{

                ttl: 60,

                limit: 10,

              }],       

    `` ``storage: new ThrottlerStorageRedisService(client),

          }),

      ],

    })

    export class AIModule { }

    in the context explanation code above:

    TTL (Time-To-Live):

    • ttl stands for Time-To-Live. It is the duration (in seconds) for which a specific rate limit applies. After this time period expires, the counter for the number of requests made is reset.
    • It defines the window of time in which the requests are counted. For example, if ttl is set to 60 seconds, the rate limit will reset every minute.

    Limit:

    • limit is the maximum number of requests that a user or client can make within the specified ttl period.
    • It sets the threshold for the number of requests. For instance, if the limit is set to 10 and the ttl is 60 seconds, a user can make up to 10 requests per minute.

    Setting up Custom Throttler

    CustomThrottlerGuard is designed to handle request throttling based on both IP address and name combination. It overrides the default throttler behavior by adding custom logic to track and limit requests more granularly. This guard ensures that excessive requests from the same IP or with the same name combination are properly handled and restricted, providing a more robust and secure throttling mechanism for the application.

    import { ExecutionContext, Injectable } from '@nestjs/common';

    import { ThrottlerException, ThrottlerGuard, ThrottlerOptions } from '@nestjs/throttler';

    @Injectable()

    export class CustomThrottlerGuard extends ThrottlerGuard {

    async handleRequest(context: ExecutionContext, limit: number, ttl: number, throttler: ThrottlerOptions): Promise<boolean> {   

    const { req, res } = this.getRequestResponse(context);

    const moduleName = req.url.split('/').pop();

     // Check if user agent should be ignored   

    if (Array.isArray(throttler.ignoreUserAgents)) {     

    for (const pattern of throttler.ignoreUserAgents) {       

    if (pattern.test(req.headers['user-agent'])) {         

    return true; // Ignore throttling for this user agent       

    }     

    }   

    }

    // Track IP address   

    const ipTracker = req.ip;

    const ipKey = `jamilmuhammad_blog_${ipTracker}_${moduleName}`

            const { totalHits: totalHitsIp, timeToExpire: timeToExpireIp } = await this.storageService.increment(ipKey, ttl);

    // Check ip limit   

    if (totalHitsIp > limit) {

          res.header('Retry-After', timeToExpireIp);

          throw new ThrottlerException('Request limit exceeded for name combination.');

        `` ``}

            // Track ip and name combination

        `` ``const { name } = req.body;

        `` ``if (!name) {

          `` `` ``throw new ThrottlerException('name are required.');

    }

    const nameTracker = `${name}`;

    const nameKey = `jamilmuhammad_blog_${ipTracker}_${moduleName}_${nameTracker}`

    const { totalHits: totalHitsName, timeToExpire: timeToExpireName } = await this.storageService.increment(nameKey, ttl);

        `` ``// Check name limit

        `` ``if (totalHitsName > parseInt(process.env.NAME_MAX_TRY_COUNT)) {

          res.header('Retry-After', timeToExpireName);

          throw new ThrottlerException('Request limit exceeded for name combination.');

    }

    // Set response headers

    res.header(`${this.headerPrefix}-Limit`, limit);

    res.header(`${this.headerPrefix}-Remaining`, Math.max(0, limit - totalHitsName));    res.header(`${this.headerPrefix}-Reset`, timeToExpireName);

    return true;

    }

    protected getNameTracker(req: Record<string, any>): string {

        const { name } = req.body;

        if (!name) {

          throw new ThrottlerException('name are required.');

        }

        return `${name}`;

      }}

    in the context explanation code above:

    1. handle request method

    const { req, res } = this.getRequestResponse(context);

    const moduleName = req.url.split('/').pop();

    • extracted url to get specified service to get rate limit

    2. user logging ignoring

    if (Array.isArray(throttler.ignoreUserAgents)) {     

    for (const pattern of throttler.ignoreUserAgents) {       

    if (pattern.test(req.headers['user-agent'])) {         

    return true; // Ignore throttling for this user agent       

    }     

    }

    }

    • Checks if the user agent should be ignored from throttling based on the ignoreUserAgents patterns defined in throttler.

    3. IP Address Tracking

    // Track IP address   

    const ipTracker = req.ip;

    const ipKey = `jamilmuhammad_blog_${ipTracker}_${moduleName}`

            const { totalHits: totalHitsIp, timeToExpire: timeToExpireIp } = await this.storageService.increment(ipKey, ttl);

    // Check ip limit   

    if (totalHitsIp > limit) {

          res.header('Retry-After', timeToExpireIp);

          throw new ThrottlerException('Request limit exceeded for name combination.');

        `` ``}

    • Tracks the IP address of the request.
    • Generates a unique key ipKey for the IP address and module name.
    • Increments the request count for the IP address and checks if it exceeds the limit.

    4. IP Address and Name Combination Tracking

    // Track ip and name combination

        `` ``const { name } = req.body;

        `` ``if (!name) {

          `` `` ``throw new ThrottlerException('name are required.');

    }

    const nameTracker = `${name}`;

    const nameKey = `jamilmuhammad_blog_${ipTracker}_${moduleName}_${nameTracker}`

    const { totalHits: totalHitsName, timeToExpire: timeToExpireName } = await this.storageService.increment(nameKey, ttl);

        `` ``// Check name limit

        `` ``if (totalHitsName > parseInt(process.env.NAME_MAX_TRY_COUNT)) {

          res.header('Retry-After', timeToExpireName);

          throw new ThrottlerException('Request limit exceeded for name combination.');

    }

    • Extracts name from the request body and validates its presence.
    • Generates a unique key nameKey for the IP address, module name, and name.
    • Increments the request count for the name combination and checks if it exceeds the limit defined by NAME_MAX_TRY_COUNT.

    5. Setting Response Header

    //Set response headers

    res.header(`${this.headerPrefix}-Limit`, limit);

    res.header(`${this.headerPrefix}-Remaining`, Math.max(0, limit - totalHitsName));    res.header(`${this.headerPrefix}-Reset`, timeToExpireName);

    return true;

    • Sets the response headers to indicate the request limit, remaining requests, and time to reset.

    6. getNameTracker Method

    protected getNameTracker(req: Record<string, any>): string {

        const { name } = req.body;

        if (!name) {

          throw new ThrottlerException('name are required.');

        }

        return `${name}`;

      }}

    • A protected method to extract and validate name from the request body.
    • Throws an exception if name is not present.

    Implementing Rate Limitting in Controller

    setup ensures that the endpoint in ai.controller is protected from abuse by enforcing request limits and custom throttling rules.

    @UseGuards(CustomThrottlerGuard)

    @Post('name-generator')

    @Throttle({ default: { limit: 4, ttl: 3600} })

    aiSecretFeature(@RealIP() ip: string, @Body() body: GeneratorAIDto) {

        return this.aiService.secretFeatureAI(body);

    }

    in the context explanation code above:

    • The @UseGuards(CustomThrottlerGuard) decorator applies a custom throttling guard to this endpoint.
    • The @Post('name-generator') decorator defines that the method handles POST requests to /name-generator.
    • The @Throttle({ default: { limit: 4, ttl: 3600} }) decorator sets the rate limit to 4 requests per hour.
    • The aiSecretFeature method extracts the real IP address and the request body, and then calls a service method with the body data.

    if all the steps have been done, it will immediately appear in the upstash dashboard console

    if this article is useful don't forget to share and write comments. Stay tech-savvy and keep the conversation going. Cheers to a brighter, more connected future! 🚀✨