Skip to content

@RateLimit Decorator ​

Apply rate limits declaratively.

Basic Usage ​

typescript
import { Controller, Get } from '@nestjs/common';
import { RateLimit } from '@nestjs-redisx/rate-limit';

@Controller('api')
export class ApiController {
  @Get('data')
  @RateLimit({ points: 10, duration: 60 })
  getData() {
    return { data: 'value' };
  }
}

Options Reference ​

typescript
interface IRateLimitOptions {
  key?: string | KeyExtractor;
  algorithm?: 'fixed-window' | 'sliding-window' | 'token-bucket';
  points?: number;
  duration?: number;
  refillRate?: number;
  store?: 'redis' | 'memory'; // overrides the plugin-level store default
  skip?: (context: ExecutionContext) => boolean | Promise<boolean>;
  message?: string;
  errorFactory?: (result: RateLimitResult) => Error;
}

The store option selects the counter backend for this route — 'redis' (exact, shared by all instances) or 'memory' (per-instance, zero Redis round-trip) — overriding the plugin default in either direction. See Stores:

typescript
import { Controller, Get, Post } from '@nestjs/common';
import { RateLimit } from '@nestjs-redisx/rate-limit';

/**
 * Plugin default is `store: 'memory'` — bulk traffic pays zero Redis
 * round-trip. Sensitive routes override back to the exact, distributed
 * Redis store per route. The override works in either direction.
 */
@Controller()
export class ApiController {
  // Uses the plugin default store ('memory'): cheap per-instance limiting.
  @Get('feed')
  @RateLimit({ points: 100, duration: 60 })
  getFeed() {
    return { items: [] };
  }

  // Auth-sensitive route pinned to Redis: the count is exact and shared by
  // ALL instances. A distributed brute force cannot multiply the limit by
  // the node count here.
  @Post('login')
  @RateLimit({ store: 'redis', points: 5, duration: 300 })
  login() {
    return { ok: true };
  }

  // The reverse also works: with a 'redis' plugin default, a hot endpoint
  // can opt into per-instance memory counting.
  @Get('health-details')
  @RateLimit({ store: 'memory', points: 30, duration: 60 })
  healthDetails() {
    return { status: 'ok' };
  }
}

Method Decorator ​

typescript
@Controller('api')
export class ApiController {
  // 10 requests per minute per IP
  @Get('public')
  @RateLimit({ points: 10, duration: 60 })
  getPublic() {
    return { public: true };
  }

  // 100 requests per minute per user
  @Get('private')
  @RateLimit({ points: 100, duration: 60, key: 'user' })
  getPrivate() {
    return { private: true };
  }

  // Token bucket: 50 capacity, 5 tokens/sec refill
  @Get('stream')
  @RateLimit({
    algorithm: 'token-bucket',
    points: 50,
    refillRate: 5,
  })
  getStream() {
    return { stream: true };
  }
}

Class Decorator ​

Apply to all methods in controller:

typescript
@Controller('api')
@RateLimit({ points: 100, duration: 60 })
export class ApiController {
  @Get('endpoint1')  // Inherits 100/min
  endpoint1() {}

  @Get('endpoint2')  // Inherits 100/min
  endpoint2() {}

  @Get('special')
  @RateLimit({ points: 10, duration: 60 })  // Override: 10/min
  special() {}
}

Key Patterns ​

Built-in Keys ​

typescript
// IP address (default)
@RateLimit({ points: 100 })
// Redis key: rl:{algorithm}:192.168.1.1

// User ID (from request.user.id)
@RateLimit({ points: 100, key: 'user' })
// Redis key: rl:{algorithm}:user:123

// API key (from X-API-Key or Authorization header)
@RateLimit({ points: 100, key: 'apiKey' })
// Redis key: rl:{algorithm}:apikey:sk_live_xxx

// Static key (global limit)
@RateLimit({ points: 10000, key: 'global' })
// Redis key: rl:{algorithm}:global

Custom Key Function ​

typescript
// By tenant
@RateLimit({
  key: (ctx) => {
    const req = ctx.switchToHttp().getRequest();
    return `tenant:${req.headers['x-tenant-id']}`;
  },
  points: 1000,
})

// By route + user
@RateLimit({
  key: (ctx) => {
    const req = ctx.switchToHttp().getRequest();
    return `${req.path}:${req.user?.id || req.ip}`;
  },
  points: 50,
})

Skip Conditions ​

typescript
// Skip for admins
@RateLimit({
  points: 10,
  skip: (ctx) => {
    const req = ctx.switchToHttp().getRequest();
    return req.user?.role === 'admin';
  },
})

// Skip for premium
@RateLimit({
  points: 10,
  skip: async (ctx) => {
    const req = ctx.switchToHttp().getRequest();
    const user = await this.userService.getUser(req.user.id);
    return user.plan === 'premium';
  },
})

// Skip for internal IPs
@RateLimit({
  points: 100,
  skip: (ctx) => {
    const req = ctx.switchToHttp().getRequest();
    return req.ip.startsWith('10.') || req.ip.startsWith('192.168.');
  },
})

Custom Errors ​

Custom Message ​

typescript
@RateLimit({
  points: 10,
  message: 'Too many requests. Try again in a minute.',
})

Custom Error Factory ​

typescript
@RateLimit({
  points: 10,
  errorFactory: (result) => {
    return new HttpException({
      statusCode: 429,
      message: 'Rate limit exceeded',
      retryAfter: result.retryAfter,
      limit: result.limit,
      remaining: result.remaining,
    }, 429);
  },
})

Algorithm Selection ​

typescript
// Fixed Window: simple, low memory
@RateLimit({
  algorithm: 'fixed-window',
  points: 100,
  duration: 60,
})

// Sliding Window: accurate (default)
@RateLimit({
  algorithm: 'sliding-window',
  points: 100,
  duration: 60,
})

// Token Bucket: smooth with burst
@RateLimit({
  algorithm: 'token-bucket',
  points: 100,       // Bucket capacity
  refillRate: 10,    // 10 tokens/second
})

Combining Limits ​

Multiple Decorators ​

typescript
@Get('endpoint')
@RateLimit({ points: 100, duration: 60, key: 'user' })   // 100/min per user
@RateLimit({ points: 10, duration: 1, key: 'user' })     // 10/sec per user
endpoint() {}

Global + Per-User ​

typescript
@Controller('api')
@RateLimit({ points: 10000, duration: 60, key: 'global' })  // Global: 10K/min
export class ApiController {
  @Get('data')
  @RateLimit({ points: 100, duration: 60, key: 'user' })    // + Per user: 100/min
  getData() {}
}

Real-World Examples ​

Login Endpoint ​

typescript
@Post('login')
@RateLimit({
  points: 5,
  duration: 900,  // 15 minutes
  message: 'Too many login attempts. Try again in 15 minutes.',
})
async login(@Body() dto: LoginDto) {
  return this.authService.login(dto);
}

File Upload ​

typescript
@Post('upload')
@RateLimit({
  algorithm: 'token-bucket',
  points: 10,        // 10 concurrent
  refillRate: 0.5,   // 1 every 2 seconds
})
@UseInterceptors(FileInterceptor('file'))
async upload(@UploadedFile() file: Express.Multer.File) {
  return this.fileService.save(file);
}

Expensive Operation ​

typescript
@Post('report')
@RateLimit({
  points: 3,
  duration: 3600,   // 3 per hour
  key: 'user',
  message: 'Report generation limited to 3 per hour',
})
async generateReport(@Body() dto: ReportDto) {
  return this.reportService.generate(dto);
}

Next Steps ​

Released under the MIT License.