NestJS Throttler Rate Limiting

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

WebSocketGraphQL 컨텍스트에서도 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 초과 빈도를 추적하여 정상 사용자에게 영향이 없는지 확인하세요
위로 스크롤
WordPress Appliance - Powered by TurnKey Linux