Rate Limiting with Throttler

@nestjs/throttler

Rate limiting stops brute-force login attempts, OTP abuse and scrapers.

Example

npm i @nestjs/throttler

Example 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