Careeroza
Rate Limiting with Throttler
@nestjs/throttler
Rate limiting stops brute-force login attempts, OTP abuse and scrapers.
Example
npm i @nestjs/throttlerExample output
added 1 package in 1s
// app.module.ts
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
@Module({
imports: [
ThrottlerModule.forRoot([
{ name: 'short', ttl: 1000, limit: 10 }, // 10 requests per second
{ name: 'long', ttl: 60_000, limit: 200 }, // 200 per minute
]),
],
providers: [{ provide: APP_GUARD, useClass: ThrottlerGuard }],
})
export class AppModule {}
// auth.controller.ts: stricter limit for OTP
// @Throttle({ long: { limit: 3, ttl: 60_000 } })
// @Post('otp/send') send() {}
//
// @SkipThrottle() @Get('health') health() {}Example
for i in 1 2 3 4; do curl -s -o /dev/null -w "%{http_code} " -X POST localhost:3000/auth/otp/send -H "Content-Type: application/json" -d '{"phone":"+919876543210"}'; done; echo
curl -i -X POST localhost:3000/auth/otp/send -H "Content-Type: application/json" -d '{"phone":"+919876543210"}'Example output
201 201 201 429
HTTP/1.1 429 Too Many Requests
Retry-After-long: 57
X-RateLimit-Limit-long: 3
X-RateLimit-Remaining-long: 0
{"statusCode":429,"message":"ThrottlerException: Too Many Requests"}Note: The default storage is in memory, per instance. With several instances, use a Redis storage (for example @nest-lab/throttler-storage-redis), and behind a proxy set app.set('trust proxy', 1) so limits use the real client IP.
A Sliding Window Limiter
Example
Throttler-style limiting: keep the timestamps of recent hits per key and reject when there are too many inside the window. Fake clock for stable output:
class RateLimiter {
private hits = new Map<string, number[]>();
constructor(private limit: number, private windowMs: number) {}
hit(key: string, now: number): { allowed: boolean; remaining: number; retryAfterMs: number } {
const recent = (this.hits.get(key) ?? []).filter((t) => t > now - this.windowMs);
if (recent.length >= this.limit) {
return { allowed: false, remaining: 0, retryAfterMs: recent[0] + this.windowMs - now };
}
recent.push(now);
this.hits.set(key, recent);
return { allowed: true, remaining: this.limit - recent.length, retryAfterMs: 0 };
}
}
const otp = new RateLimiter(3, 60_000); // 3 OTPs per minute per phone
const times = [0, 5_000, 10_000, 20_000, 61_000, 62_000];
for (const t of times) {
const r = otp.hit('+919876543210', t);
console.log('t=' + t / 1000 + 's', r.allowed ? '201 remaining=' + r.remaining : '429 retry after ' + r.retryAfterMs / 1000 + 's');
}
console.log('other phone:', otp.hit('+919812345678', 62_000).allowed ? '201' : '429');Output
t=0s 201 remaining=2 t=5s 201 remaining=1 t=10s 201 remaining=0 t=20s 429 retry after 40s t=61s 201 remaining=0 t=62s 429 retry after 3s other phone: 201