Key Extraction
Determine WHO is being rate limited.
Built-in Extractors
| Extractor | Extracted Key | Use Case |
|---|---|---|
ip | 192.168.1.1 | Public APIs |
user | user:123 | Authenticated APIs |
apiKey | apikey:sk_xxx | API platforms |
| Static string | {string} | Global limits |
Redis Key Format
The full Redis key is built by RateLimitService as {keyPrefix}{algorithm}:{extractedKey}. For example, with default settings, the key user:123 becomes rl:sliding-window:user:123 in Redis.
IP Address (Default)
Limits by client IP address.
// Default behavior
@RateLimit({ points: 100 })
// Explicit
@RateLimit({ key: 'ip', points: 100 })Extracting Real IP (behind a proxy)
By default the IP comes from the framework (request.ip) and is not taken from client-supplied headers. This is secure: a client cannot spoof X-Forwarded-For to get a fresh bucket per request (which would bypass the limit on login/password-reset endpoints).
When you run behind a proxy or load balancer, tell the framework to trust it — then request.ip becomes the real, un-spoofable client IP:
// Express
app.set('trust proxy', 1); // number of proxies in front of you
// Fastify
// new FastifyAdapter({ trustProxy: true })Do not blindly trust forwarding headers
trustProxy: true on the plugin makes it read X-Forwarded-For / X-Real-IP directly. Enable it ONLY when a trusted proxy overwrites those headers; a public-facing app that trusts them lets any client spoof its IP.
new RateLimitPlugin({ trustProxy: true }) // opt-in, behind a trusted proxy onlyWhen to Use
- Public endpoints without authentication
- Login/registration endpoints
- Default for unknown clients
Limitations
- NAT: many users share IP
- VPN: users can change IP
- Proxies: may hide real IP
User ID
Limits by authenticated user.
@RateLimit({ key: 'user', points: 100 })How It Works
// Expects request.user.id to be set
// Usually by authentication guard
@UseGuards(AuthGuard, RateLimitGuard)
@RateLimit({ key: 'user', points: 100 })
async getProtectedData() {}When to Use
- Authenticated endpoints
- Per-user quotas
- Fair usage across users
Requirements
- Authentication guard must run BEFORE rate limit guard
request.user.idmust be populated
API Key
Limits by API key.
@RateLimit({ key: 'apiKey', points: 1000 })How It Works
// Reads from X-API-Key header
// Request: GET /api/data
// X-API-Key: sk_live_abc123
// Key becomes: rl:apikey:sk_live_abc123Custom Header
new RateLimitPlugin({
defaultKeyExtractor: (ctx) => {
const req = ctx.switchToHttp().getRequest();
return req.headers['authorization']?.replace('Bearer ', '') || req.ip;
},
})When to Use
- API platforms
- Third-party integrations
- Different limits per key tier
Static Key
Global limit across all clients.
@RateLimit({ key: 'global', points: 10000 })Use Cases
// Global API limit
@RateLimit({ key: 'global:api', points: 100000, duration: 60 })
// Per-endpoint global limit
@RateLimit({ key: 'global:expensive', points: 100, duration: 60 })
async expensiveOperation() {}When to Use
- Protect expensive operations globally
- Server-wide capacity limits
- Combined with per-user limits
Custom Key Functions
Full control over key generation.
type KeyExtractor = (context: ExecutionContext) => string | Promise<string>;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();
const route = req.route.path;
const user = req.user?.id || req.ip;
return `${route}:${user}`;
},
points: 50,
})By Organization
@RateLimit({
key: async (ctx) => {
const req = ctx.switchToHttp().getRequest();
const orgId = await this.orgService.getOrgForUser(req.user.id);
return `org:${orgId}`;
},
points: 5000,
})Composite Key
@RateLimit({
key: (ctx) => {
const req = ctx.switchToHttp().getRequest();
// Different limits for different user types
const tier = req.user?.tier || 'free';
return `${tier}:${req.user?.id || req.ip}`;
},
})Combining Keys
IP + User
@Controller('api')
export class ApiController {
@Get('data')
@RateLimit({ key: 'ip', points: 1000, duration: 60 }) // 1000/min per IP
@RateLimit({ key: 'user', points: 100, duration: 60 }) // 100/min per user
getData() {}
}Global + Per-User
@Controller('api')
@RateLimit({ key: 'global', points: 10000, duration: 60 }) // Global cap
export class ApiController {
@Get('data')
@RateLimit({ key: 'user', points: 100, duration: 60 }) // Per-user limit
getData() {}
}Best Practices
Do
// Use specific keys
key: `user:${req.user.id}`
// Combine multiple layers
@RateLimit({ key: 'ip' }) // Prevent abuse
@RateLimit({ key: 'user' }) // Fair usage
// Handle missing identifiers
key: (ctx) => req.user?.id || `anon:${req.ip}`Don't
// Use sensitive data in keys
key: `user:${req.user.email}` // PII in Redis!
// Very long keys
key: `${JSON.stringify(req.body)}` // Memory waste
// Forget fallback
key: (ctx) => req.user.id // Fails if not authenticated