NestJS API Rate Limiting Guide: Protect Your SaaS
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 →</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.
- Free version: GitHub
- Pro ($249): Get SaaS Starter Pro