Skip to content
← Back to blog
NestJSRate LimitingAPISecurity

NestJS API Rate Limiting Guide: Protect Your SaaS

Firas Sayah·April 8, 2026·5 min read

Why Rate Limiting Is Essential

Without rate limiting, a single bad actor or misbehaving integration can take down your entire SaaS. Rate limiting protects your infrastructure, ensures fair usage across tenants, and is a requirement for any public-facing API. NestJS makes this straightforward with the built-in throttler module and custom enhancements.

Basic Setup with @nestjs/throttler

npm install @nestjs/throttler

Configure the throttler module globally:

import { ThrottlerModule } from '@nestjs/throttler';

@Module({
  imports: [
    ThrottlerModule.forRoot({
      throttlers: [
        { name: 'short', ttl: 1000, limit: 10 },   // 10 req/sec
        { name: 'medium', ttl: 60000, limit: 100 }, // 100 req/min
        { name: 'long', ttl: 3600000, limit: 1000 }, // 1000 req/hour
      ],
    }),
  ],
})
export class AppModule {}

Apply it globally or per controller:

@UseGuards(ThrottlerGuard)
@Controller('api')
export class ApiController {}

Redis-Backed Rate Limiting

The default in-memory storage does not work in a multi-instance deployment. Switch to Redis:

import { ThrottlerStorageRedisService } from '@nestjs/throttler-storage-redis';

ThrottlerModule.forRoot({
  throttlers: [{ ttl: 60000, limit: 100 }],
  storage: new ThrottlerStorageRedisService(new Redis({
    host: 'localhost',
    port: 6379,
  })),
})

Redis ensures rate limit counts are shared across all your API server instances.

Per-Plan Rate Limits

SaaS products need different limits per subscription tier. Create a custom guard:

@Injectable()
export class PlanBasedThrottlerGuard extends ThrottlerGuard {
  private planLimits: Record<string, number> = {
    free: 100,
    starter: 1000,
    professional: 5000,
    enterprise: 50000,
  };

<div class="not-prose my-8 rounded-xl border border-primary-200 dark:border-primary-800 bg-primary-50 dark:bg-primary-900/20 p-6">
<p class="text-base font-semibold text-primary-900 dark:text-primary-100 mb-1">Cloudrix SaaS Starter ships with this pre-configured and tested.</p>
<p class="text-sm text-primary-700 dark:text-primary-300 mb-3">Skip weeks of boilerplate — auth, payments, multi-tenancy, and deployment included out of the box.</p>
<a href="https://demo.cloudrix.io" class="inline-flex items-center text-sm font-medium text-primary-600 dark:text-primary-400 hover:text-primary-500">Try the live demo &rarr;</a>
</div>


  async getLimit(context: ExecutionContext): Promise<number> {
    const request = context.switchToHttp().getRequest();
    const plan = request.user?.subscription?.plan ?? 'free';
    return this.planLimits[plan] ?? this.planLimits.free;
  }
}

Custom Rate Limit Decorator

Create a decorator for endpoint-specific limits:

export const RateLimit = (limit: number, ttl: number) =>
  SetMetadata('rateLimit', { limit, ttl });

// Usage
@RateLimit(5, 60000) // 5 requests per minute
@Post('expensive-operation')
async expensiveOperation() { ... }

Rate Limit Headers

Inform API consumers about their remaining quota with standard headers:

@Injectable()
export class RateLimitHeadersInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    return next.handle().pipe(
      tap(() => {
        const response = context.switchToHttp().getResponse();
        const request = context.switchToHttp().getRequest();
        response.setHeader('X-RateLimit-Limit', request.rateLimit?.limit);
        response.setHeader('X-RateLimit-Remaining', request.rateLimit?.remaining);
        response.setHeader('X-RateLimit-Reset', request.rateLimit?.reset);
      }),
    );
  }
}

These headers follow the standard convention used by GitHub, Stripe, and other major APIs. Your customers' integration developers will appreciate the transparency.

Sliding Window Algorithm

The fixed window approach has a known edge case: a burst of requests at the boundary of two windows allows double the limit. Use a sliding window for more accurate limiting:

async isRateLimited(key: string, limit: number, windowMs: number): Promise<boolean> {
  const now = Date.now();
  const windowStart = now - windowMs;

  const pipeline = this.redis.pipeline();
  pipeline.zremrangebyscore(key, 0, windowStart);
  pipeline.zadd(key, now, `${now}-${Math.random()}`);
  pipeline.zcard(key);
  pipeline.expire(key, Math.ceil(windowMs / 1000));

  const results = await pipeline.exec();
  const count = results[2][1] as number;
  return count > limit;
}

API Key Rate Limiting

For public APIs, rate limit by API key rather than user session:

protected getTracker(req: Request): string {
  const apiKey = req.headers['x-api-key'] as string;
  return apiKey ?? req.ip;
}

Handling Rate Limit Exceeded

Return a clear error with retry information:

@Catch(ThrottlerException)
export class RateLimitExceptionFilter implements ExceptionFilter {
  catch(exception: ThrottlerException, host: ArgumentsHost) {
    const response = host.switchToHttp().getResponse();
    response.status(429).json({
      statusCode: 429,
      message: 'Rate limit exceeded. Please slow down.',
      retryAfter: 60,
    });
  }
}

Get Started Today

All of these patterns are fully implemented in SaaS Starter. Skip 2-6 weeks of setup.

F

Firas Sayah

Senior Software Engineer

Full-stack developer with 5+ years building production SaaS applications with NestJS and Angular.