Hero image for A Pragmatic Guide to Rate Limiting: NestJS + Redis + Nginx

A Pragmatic Guide to Rate Limiting: NestJS + Redis + Nginx

• 4 min read
NestJS Redis Nginx Rate Limiting Backend Security API Design

🚦 “Rate limiting helps prevent service abuse, and protects performance and user experience.”


When building production APIs, rate limiting is one of those things that often gets added late — usually after the first abuse spike or load test failure.

I’ve found that adding basic rate limiting early, even if it’s not perfect, is a worthwhile trade-off. It doesn’t need to be complicated, but it should be layered. This post walks through how I implement it using NestJS, Redis, and optionally Nginx.

🧱 Why Layered Rate Limiting?

I like to think about rate limiting in two layers:

Using both gives you a fast first line of defense, and a more flexible backend layer for finer-grained control.

1. Nginx Rate Limiting (Quick, Simple, Blunt)

In most of my deployments, I use Nginx as a reverse proxy in front of the NestJS API. Here’s a basic config:

http {
  limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

  server {
    location /api/ {
      limit_req zone=api_limit burst=20 nodelay;
      proxy_pass http://nestjs_upstream;
      # other proxy settings...
    }
  }
}

What this does:

It isn’t perfect, but it is sufficiently fast and uses no extra resources inside your app.

2. App-Level Rate Limiting with NestJS + Redis

At the application level, for more flexibility:

Step 1: Install Dependencies

pnpm add rate-limiter-flexible ioredis

Step 2: Set Up the Redis Client

// src/redis/redis.service.ts
import { Redis } from 'ioredis';

export const redis = new Redis({
  host: 'redis',
  port: 6379,
});

Step 3: Create a Rate Limiter Guard

// src/common/guards/rate-limit.guard.ts
import {
  CanActivate,
  ExecutionContext,
  Injectable,
  TooManyRequestsException,
} from '@nestjs/common';
import { RateLimiterRedis } from 'rate-limiter-flexible';
import { redis } from '../../redis/redis.service';

const rateLimiter = new RateLimiterRedis({
  storeClient: redis,
  keyPrefix: 'rl',
  points: 10, // 10 requests
  duration: 60, // per 60 seconds
});

@Injectable()
export class RateLimitGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req = context.switchToHttp().getRequest();
    const key = req.ip || 'anonymous';

    try {
      await rateLimiter.consume(key);
      return true;
    } catch {
      throw new TooManyRequestsException(
        'Too many requests. Please try again later.',
      );
    }
  }
}

Step 4: Apply the Guard

// src/app.controller.ts
import { UseGuards, Controller, Get } from '@nestjs/common';
import { RateLimitGuard } from './common/guards/rate-limit.guard';

@Controller('api')
export class AppController {
  @UseGuards(RateLimitGuard)
  @Get('public-endpoint')
  getStuff() {
    return { ok: true };
  }
}

You can also apply it globally using app.useGlobalGuards().

3. Optional: Customize by User or API Key

const userId = req.user?.id || req.headers['x-api-key'] || req.ip;
await rateLimiter.consume(userId);

This gives you more precise control — and you can adjust limits dynamically if needed.

🧪 Bonus: Test It in CI or Locally

If you’re using tools like supertest, add integration tests to ensure 429s are thrown when expected:

it('should rate limit after 10 requests', async () => {
  for (let i = 0; i < 11; i++) {
    await request(app.getHttpServer()).get('/api/public-endpoint');
  }

  const res = await request(app.getHttpServer()).get('/api/public-endpoint');
  expect(res.status).toBe(429);
});

Final Thoughts

Rate limiting doesn’t have to be perfect, but it’s worth doing early — especially if your API is public or unauthenticated.

I’ve found that combining:

…gives you a setup that’s easy to maintain and hard to abuse.

If it prevents even one outage or abuse spike, it’s time well spent.

🏷 Tags

NestJS · Redis · Nginx · Backend Security · API Rate Limiting · Performance