A Pragmatic Guide to Rate Limiting: NestJS + Redis + Nginx
How I implement layered rate limiting in full-stack applications using NestJS, Redis, and Nginx — with performance and maintainability in mind.
Rate limiting protects service quality, 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:
- Edge-level (Nginx) – Blocks requests before they hit your app
- App-level (NestJS + Redis) – Enforces per-user or per-IP rules with more logic
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:
- 10 requests/second per IP
- Allows small bursts of up to 20 requests
- Rejects excess traffic with 503
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:
- Per-user, not just IP
- Track API keys or auth headers
- Return JSON errors, not HTML
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:
- Nginx for simple, IP-based throttling
- Redis + NestJS for granular limits
- Optional API key or session-based tracking
…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.
Next