NestJS Throttler란?
@nestjs/throttler는 NestJS 공식 Rate Limiting 모듈입니다. API 엔드포인트에 요청 빈도를 제한하여 DDoS 공격, 브루트포스 시도, API 남용을 방지합니다. 데코레이터 기반의 선언적 설정, 다중 제한 정책, Redis 분산 스토어, WebSocket·GraphQL 지원 등 프로덕션 수준의 기능을 제공합니다.
기본 설정
ThrottlerModule을 글로벌로 등록하고, ThrottlerGuard를 전역 가드로 설정합니다.
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { APP_GUARD } from '@nestjs/core';
@Module({
imports: [
ThrottlerModule.forRoot([
{
name: 'short',
ttl: 1000, // 1초
limit: 3, // 초당 3회
},
{
name: 'medium',
ttl: 10000, // 10초
limit: 20, // 10초당 20회
},
{
name: 'long',
ttl: 60000, // 1분
limit: 100, // 분당 100회
},
]),
],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule {}
다중 제한 정책(short/medium/long)을 동시에 적용하면 버스트 트래픽과 지속적 남용 모두를 방어할 수 있습니다. 모든 정책을 통과해야 요청이 허용됩니다.
엔드포인트별 커스텀 제한
@Throttle()과 @SkipThrottle() 데코레이터로 개별 엔드포인트의 제한을 조정합니다.
@Controller('auth')
export class AuthController {
// 로그인: 더 엄격한 제한 (브루트포스 방지)
@Post('login')
@Throttle({ short: { ttl: 1000, limit: 1 }, long: { ttl: 60000, limit: 5 } })
async login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
// 비밀번호 리셋: 매우 엄격
@Post('reset-password')
@Throttle({ short: { ttl: 1000, limit: 1 }, long: { ttl: 3600000, limit: 3 } })
async resetPassword(@Body() dto: ResetPasswordDto) {
return this.authService.resetPassword(dto);
}
// 토큰 검증: Rate Limiting 제외
@Get('verify')
@SkipThrottle()
async verify(@Query('token') token: string) {
return this.authService.verify(token);
}
}
@Controller('products')
// 컨트롤러 전체에서 특정 정책만 제외
@SkipThrottle({ short: true }) // short 정책만 스킵
export class ProductsController {
@Get()
findAll() {
return this.productsService.findAll();
}
// 이 엔드포인트만 모든 정책 스킵
@Get('health')
@SkipThrottle()
health() {
return { ok: true };
}
}
커스텀 키 추적: IP, 사용자, API 키
기본적으로 클라이언트 IP로 요청을 추적하지만, 사용자 ID나 API 키 기반으로 변경할 수 있습니다. ThrottlerGuard를 확장합니다.
import { ThrottlerGuard, ThrottlerRequest } from '@nestjs/throttler';
import { ExecutionContext, Injectable } from '@nestjs/common';
@Injectable()
export class CustomThrottlerGuard extends ThrottlerGuard {
// 추적 키 커스터마이징
protected async getTracker(req: Record<string, any>): Promise<string> {
// 인증된 사용자: userId 기반
if (req.user?.id) {
return `user-${req.user.id}`;
}
// API 키 사용자: API 키 기반
const apiKey = req.headers['x-api-key'];
if (apiKey) {
return `api-${apiKey}`;
}
// 미인증: IP 기반
return req.ip;
}
// 특정 조건에서 Rate Limiting 건너뛰기
protected async shouldSkip(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
// 관리자는 Rate Limiting 제외
if (request.user?.role === 'ADMIN') {
return true;
}
// 내부 서비스 호출은 제외
if (request.headers['x-internal-service'] === 'true') {
return true;
}
return false;
}
}
// 모듈에 커스텀 가드 등록
@Module({
providers: [
{
provide: APP_GUARD,
useClass: CustomThrottlerGuard,
},
],
})
export class AppModule {}
Redis 분산 스토어
다중 인스턴스 환경에서는 인메모리 스토어 대신 Redis를 사용해야 합니다. 인스턴스 간 Rate Limit 카운터를 공유하여 일관된 제한을 적용합니다.
import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis';
import Redis from 'ioredis';
@Module({
imports: [
ThrottlerModule.forRoot({
throttlers: [
{ name: 'short', ttl: 1000, limit: 3 },
{ name: 'long', ttl: 60000, limit: 100 },
],
storage: new ThrottlerStorageRedisService(
new Redis({
host: 'redis.internal',
port: 6379,
keyPrefix: 'throttle:',
}),
),
}),
],
})
export class AppModule {}
// 비동기 설정 (ConfigService 활용)
ThrottlerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
throttlers: [
{
name: 'default',
ttl: config.get('THROTTLE_TTL', 60000),
limit: config.get('THROTTLE_LIMIT', 100),
},
],
storage: new ThrottlerStorageRedisService(
new Redis(config.get('REDIS_URL')),
),
}),
})
응답 헤더와 에러 커스터마이징
Rate Limit 상태를 응답 헤더로 전달하고, 제한 초과 시 커스텀 에러 응답을 반환합니다.
@Injectable()
export class DetailedThrottlerGuard extends ThrottlerGuard {
protected async throwThrottlingException(
context: ExecutionContext,
throttlerLimitDetail: any,
): Promise<void> {
const response = context.switchToHttp().getResponse();
response.header('Retry-After', Math.ceil(throttlerLimitDetail.ttl / 1000));
response.header('X-RateLimit-Limit', throttlerLimitDetail.limit);
response.header('X-RateLimit-Reset',
new Date(Date.now() + throttlerLimitDetail.ttl).toISOString());
throw new HttpException(
{
statusCode: 429,
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many requests. Please try again later.',
retryAfter: Math.ceil(throttlerLimitDetail.ttl / 1000),
},
HttpStatus.TOO_MANY_REQUESTS,
);
}
}
// 에러 응답 예시:
// HTTP 429 Too Many Requests
// Retry-After: 60
// X-RateLimit-Limit: 100
// X-RateLimit-Reset: 2026-03-09T23:03:00.000Z
// {
// "statusCode": 429,
// "code": "RATE_LIMIT_EXCEEDED",
// "message": "Too many requests. Please try again later.",
// "retryAfter": 60
// }
WebSocket·GraphQL Rate Limiting
WebSocket과 GraphQL 컨텍스트에서도 Rate Limiting을 적용할 수 있습니다.
// WebSocket Gateway에 Throttler 적용
@WebSocketGateway()
@UseGuards(WsThrottlerGuard)
export class ChatGateway {
@SubscribeMessage('message')
@Throttle({ default: { ttl: 1000, limit: 5 } }) // 초당 5메시지
handleMessage(@MessageBody() data: string) {
return { event: 'message', data };
}
}
// WebSocket 전용 ThrottlerGuard
@Injectable()
export class WsThrottlerGuard extends ThrottlerGuard {
protected async getTracker(req: Record<string, any>): Promise<string> {
// WebSocket 클라이언트 식별
return req.handshake?.address || req.conn?.remoteAddress || 'unknown';
}
protected getRequestResponse(context: ExecutionContext) {
const client = context.switchToWs().getClient();
return { req: client, res: client };
}
}
// GraphQL: HTTP 기반이므로 기본 ThrottlerGuard 동작
// resolver 레벨에서 @Throttle 적용
@Resolver(() => User)
export class UserResolver {
@Query(() => [User])
@Throttle({ default: { ttl: 10000, limit: 10 } })
async users() {
return this.userService.findAll();
}
}
플랜별 Rate Limiting 전략
SaaS 애플리케이션에서 사용자 플랜에 따라 다른 Rate Limit을 적용하는 패턴입니다.
@Injectable()
export class PlanBasedThrottlerGuard extends ThrottlerGuard {
private readonly planLimits = {
free: { ttl: 60000, limit: 30 },
starter: { ttl: 60000, limit: 100 },
pro: { ttl: 60000, limit: 500 },
enterprise: { ttl: 60000, limit: 5000 },
};
protected async getLimit(context: ExecutionContext): Promise<number> {
const request = context.switchToHttp().getRequest();
const plan = request.user?.plan || 'free';
return this.planLimits[plan]?.limit || this.planLimits.free.limit;
}
protected async getTtl(context: ExecutionContext): Promise<number> {
const request = context.switchToHttp().getRequest();
const plan = request.user?.plan || 'free';
return this.planLimits[plan]?.ttl || this.planLimits.free.ttl;
}
}
운영 베스트 프랙티스
- 다중 시간 윈도우: short(초)/medium(10초)/long(분) 다중 정책으로 버스트와 지속 남용을 동시에 방어하세요
- Redis 분산 스토어: 다중 인스턴스 환경에서는 반드시 Redis 스토어를 사용하세요
- 인증 엔드포인트 강화: 로그인, 비밀번호 리셋 등은 더 엄격한 제한을 적용하세요
- Retry-After 헤더: 429 응답 시 클라이언트에 재시도 시점을 알려주세요
- 관리자/내부 서비스 제외:
shouldSkip으로 신뢰할 수 있는 요청은 Rate Limiting에서 제외하세요 - 모니터링: Rate Limit 초과 빈도를 추적하여 정상 사용자에게 영향이 없는지 확인하세요