← Back to Tutorials
Intermediate
20 min
Rate Limiting with Redis
Protect your APIs from abuse and ensure fair resource allocation using Redis-based rate limiting strategies.
What You'll Learn
- Fixed window rate limiting
- Sliding window rate limiting
- Token bucket algorithm
- Express middleware integration
1
Fixed Window Rate LimiterSimple and efficient rate limiting strategy
1import { SolidisFeaturedClient } from '@vcms-io/solidis/featured';
2
3export class FixedWindowRateLimiter {
4 private client: SolidisFeaturedClient;
5 private prefix: string;
6
7 constructor(options: { host?: string; port?: number; prefix?: string } = {}) {
8 this.client = new SolidisFeaturedClient({
9 host: options.host || '127.0.0.1',
10 port: options.port || 6379,
11 });
12 this.prefix = options.prefix || 'rate:';
13 }
14
15 async connect(): Promise<void> {
16 await this.client.connect();
17 }
18
19 /**
20 * Check if request is allowed
21 * @param key - Identifier (e.g., user ID, IP address)
22 * @param limit - Maximum requests allowed
23 * @param windowSeconds - Time window in seconds
24 */
25 async checkLimit(
26 key: string,
27 limit: number,
28 windowSeconds: number
29 ): Promise<{ allowed: boolean; remaining: number; resetAt: Date }> {
30 const now = Date.now();
31 const windowStart = Math.floor(now / (windowSeconds * 1000));
32 const cacheKey = `${this.prefix}${key}:${windowStart}`;
33
34 // Increment counter
35 const count = await this.client.incr(cacheKey);
36
37 // Set expiry on first request
38 if (count === 1) {
39 await this.client.expire(cacheKey, windowSeconds);
40 }
41
42 const allowed = count <= limit;
43 const remaining = Math.max(0, limit - count);
44 const resetAt = new Date((windowStart + 1) * windowSeconds * 1000);
45
46 return { allowed, remaining, resetAt };
47 }
48}2
Sliding Window Rate LimiterMore accurate rate limiting with smooth distribution
1export class SlidingWindowRateLimiter {
2 private client: SolidisFeaturedClient;
3 private prefix: string;
4
5 constructor(options: { host?: string; port?: number; prefix?: string } = {}) {
6 this.client = new SolidisFeaturedClient({
7 host: options.host || '127.0.0.1',
8 port: options.port || 6379,
9 });
10 this.prefix = options.prefix || 'rate:sliding:';
11 }
12
13 async connect(): Promise<void> {
14 await this.client.connect();
15 }
16
17 /**
18 * Check if request is allowed using sliding window
19 */
20 async checkLimit(
21 key: string,
22 limit: number,
23 windowSeconds: number
24 ): Promise<{ allowed: boolean; remaining: number; retryAfter: number }> {
25 const cacheKey = `${this.prefix}${key}`;
26 const now = Date.now();
27 const windowStart = now - windowSeconds * 1000;
28
29 // Remove old entries
30 await this.client.zremrangebyscore(cacheKey, 0, windowStart);
31
32 // Count requests in window
33 const count = await this.client.zcard(cacheKey);
34
35 const allowed = count < limit;
36
37 if (allowed) {
38 // Add current request
39 await this.client.zadd(cacheKey, now, `${now}`);
40 // Set expiry
41 await this.client.expire(cacheKey, windowSeconds);
42 }
43
44 const remaining = Math.max(0, limit - count - (allowed ? 1 : 0));
45
46 // Calculate retry after (when oldest request will expire)
47 let retryAfter = 0;
48 if (!allowed && count > 0) {
49 const oldest = await this.client.zrange(cacheKey, 0, 0, { WITHSCORES: true });
50 if (oldest.length > 0) {
51 const oldestScore = Number.parseInt(oldest[1].toString());
52 retryAfter = Math.ceil((oldestScore + windowSeconds * 1000 - now) / 1000);
53 }
54 }
55
56 return { allowed, remaining, retryAfter };
57 }
58}3
Token Bucket Rate LimiterAllow burst traffic while maintaining average rate
1export class TokenBucketRateLimiter {
2 private client: SolidisFeaturedClient;
3 private prefix: string;
4
5 constructor(options: { host?: string; port?: number; prefix?: string } = {}) {
6 this.client = new SolidisFeaturedClient({
7 host: options.host || '127.0.0.1',
8 port: options.port || 6379,
9 });
10 this.prefix = options.prefix || 'rate:bucket:';
11 }
12
13 async connect(): Promise<void> {
14 await this.client.connect();
15 }
16
17 /**
18 * Check if request is allowed using token bucket
19 * @param capacity - Maximum tokens in bucket
20 * @param refillRate - Tokens added per second
21 */
22 async checkLimit(
23 key: string,
24 capacity: number,
25 refillRate: number
26 ): Promise<{ allowed: boolean; tokens: number; retryAfter: number }> {
27 const cacheKey = `${this.prefix}${key}`;
28 const now = Date.now();
29
30 // Get bucket state
31 const result = await this.client.hmget(cacheKey, 'tokens', 'lastRefill');
32 let tokens = result[0] ? Number.parseFloat(result[0].toString()) : capacity;
33 let lastRefill = result[1] ? Number.parseInt(result[1].toString()) : now;
34
35 // Calculate tokens to add
36 const timePassed = (now - lastRefill) / 1000;
37 const tokensToAdd = timePassed * refillRate;
38 tokens = Math.min(capacity, tokens + tokensToAdd);
39
40 const allowed = tokens >= 1;
41
42 if (allowed) {
43 tokens -= 1;
44 }
45
46 // Update bucket state
47 await this.client.hset(cacheKey, {
48 tokens: tokens.toString(),
49 lastRefill: now.toString(),
50 });
51 await this.client.expire(cacheKey, Math.ceil(capacity / refillRate) + 60);
52
53 const retryAfter = allowed ? 0 : Math.ceil((1 - tokens) / refillRate);
54
55 return {
56 allowed,
57 tokens: Math.floor(tokens),
58 retryAfter,
59 };
60 }
61}4
Express Middleware IntegrationEasy-to-use middleware for your Express application
1import { Request, Response, NextFunction } from 'express';
2import { SlidingWindowRateLimiter } from './sliding-window-limiter';
3
4export interface RateLimitOptions {
5 windowSeconds?: number;
6 limit?: number;
7 keyGenerator?: (req: Request) => string;
8 skip?: (req: Request) => boolean;
9 handler?: (req: Request, res: Response) => void;
10}
11
12export function createRateLimiter(
13 limiter: SlidingWindowRateLimiter,
14 options: RateLimitOptions = {}
15) {
16 const {
17 windowSeconds = 60,
18 limit = 100,
19 keyGenerator = (req) => req.ip || 'unknown',
20 skip = () => false,
21 handler = (req, res) => {
22 res.status(429).json({
23 error: 'Too many requests',
24 message: 'Please try again later',
25 });
26 },
27 } = options;
28
29 return async (req: Request, res: Response, next: NextFunction) => {
30 // Skip rate limiting if condition is met
31 if (skip(req)) {
32 return next();
33 }
34
35 const key = keyGenerator(req);
36
37 try {
38 const result = await limiter.checkLimit(key, limit, windowSeconds);
39
40 // Set rate limit headers
41 res.setHeader('X-RateLimit-Limit', limit.toString());
42 res.setHeader('X-RateLimit-Remaining', result.remaining.toString());
43
44 if (!result.allowed) {
45 res.setHeader('Retry-After', result.retryAfter.toString());
46 return handler(req, res);
47 }
48
49 next();
50 } catch (error) {
51 console.error('Rate limiter error:', error);
52 // Fail open - allow request on error
53 next();
54 }
55 };
56}
57
58// Usage examples
59export function createUserRateLimiter(limiter: SlidingWindowRateLimiter) {
60 return createRateLimiter(limiter, {
61 windowSeconds: 60,
62 limit: 100,
63 keyGenerator: (req) => `user:${req.user?.id || req.ip}`,
64 skip: (req) => req.user?.role === 'admin',
65 });
66}
67
68export function createAPIRateLimiter(limiter: SlidingWindowRateLimiter) {
69 return createRateLimiter(limiter, {
70 windowSeconds: 3600, // 1 hour
71 limit: 1000,
72 keyGenerator: (req) => {
73 const apiKey = req.headers['x-api-key'] as string;
74 return `api:${apiKey || req.ip}`;
75 },
76 });
77}
78
79export function createLoginRateLimiter(limiter: SlidingWindowRateLimiter) {
80 return createRateLimiter(limiter, {
81 windowSeconds: 900, // 15 minutes
82 limit: 5,
83 keyGenerator: (req) => `login:${req.ip}`,
84 handler: (req, res) => {
85 res.status(429).json({
86 error: 'Too many login attempts',
87 message: 'Please try again in 15 minutes',
88 });
89 },
90 });
91}Complete Usage Example
1import express from 'express';
2import { SlidingWindowRateLimiter } from './sliding-window-limiter';
3import {
4 createUserRateLimiter,
5 createAPIRateLimiter,
6 createLoginRateLimiter,
7} from './rate-limit-middleware';
8
9const app = express();
10const limiter = new SlidingWindowRateLimiter({
11 host: '127.0.0.1',
12 port: 6379,
13});
14
15app.use(express.json());
16
17// Global rate limiter
18app.use(createUserRateLimiter(limiter));
19
20// Stricter rate limit for login
21app.post('/api/login', createLoginRateLimiter(limiter), async (req, res) => {
22 // Login logic
23 res.json({ message: 'Login successful' });
24});
25
26// API key based rate limiting
27app.use('/api/v1', createAPIRateLimiter(limiter));
28
29app.get('/api/v1/data', (req, res) => {
30 res.json({ data: 'sensitive data' });
31});
32
33// Start server
34async function start() {
35 await limiter.connect();
36 app.listen(3000, () => {
37 console.log('Server running on http://localhost:3000');
38 });
39}
40
41start().catch(console.error);Best Practices
- ✓Choose the right algorithmFixed window for simplicity, sliding window for accuracy, token bucket for burst traffic
- ✓Always include rate limit headersHelp clients understand and respect rate limits
- ✓Fail open on errorsDon't block all traffic if Redis is down
- ✓Use different limits for different endpointsMore restrictive for expensive operations
