Rate Limiting Pattern
Protect your APIs from abuse with in-memory, Redis-based, and sliding window rate limiting implementations.
Overview#
Rate limiting controls how many requests a client can make to your API within a time window. It's essential for preventing abuse, ensuring fair usage, and protecting your infrastructure.
When to use:
- Protecting public APIs from abuse
- Implementing fair usage policies
- Preventing DDoS attacks
- Controlling costs for expensive operations
Key features:
- Multiple algorithms (fixed window, sliding window, token bucket)
- In-memory and Redis-based implementations
- Per-user and per-IP limiting
- Configurable limits per endpoint
Code Example#
In-Memory Rate Limiter#
1// lib/rate-limit.ts
2const rateLimitMap = new Map<string, { count: number; resetTime: number }>()
3
4export function rateLimit(
5 key: string,
6 limit: number,
7 windowMs: number
8): { success: boolean; remaining: number; resetIn: number } {
9 const now = Date.now()
10 const record = rateLimitMap.get(key)
11
12 if (!record || now > record.resetTime) {
13 rateLimitMap.set(key, { count: 1, resetTime: now + windowMs })
14 return { success: true, remaining: limit - 1, resetIn: windowMs }
15 }
16
17 if (record.count >= limit) {
18 return {
19 success: false,
20 remaining: 0,
21 resetIn: record.resetTime - now
22 }
23 }
24
25 record.count++
26 return {
27 success: true,
28 remaining: limit - record.count,
29 resetIn: record.resetTime - now
30 }
31}Redis Rate Limiter#
1// lib/rate-limit-redis.ts
2import { Redis } from '@upstash/redis'
3
4const redis = new Redis({
5 url: process.env.UPSTASH_REDIS_URL!,
6 token: process.env.UPSTASH_REDIS_TOKEN!
7})
8
9export async function rateLimitRedis(
10 key: string,
11 limit: number,
12 windowSeconds: number
13) {
14 const current = await redis.incr(key)
15
16 if (current === 1) {
17 await redis.expire(key, windowSeconds)
18 }
19
20 const ttl = await redis.ttl(key)
21
22 return {
23 success: current <= limit,
24 remaining: Math.max(0, limit - current),
25 resetIn: ttl > 0 ? ttl * 1000 : windowSeconds * 1000
26 }
27}Sliding Window Rate Limiter#
1// lib/rate-limit-sliding.ts
2import { Redis } from '@upstash/redis'
3
4const redis = new Redis({
5 url: process.env.UPSTASH_REDIS_URL!,
6 token: process.env.UPSTASH_REDIS_TOKEN!
7})
8
9export async function slidingWindowRateLimit(
10 key: string,
11 limit: number,
12 windowMs: number
13) {
14 const now = Date.now()
15 const windowStart = now - windowMs
16
17 // Remove old entries and add new one
18 const pipeline = redis.pipeline()
19 pipeline.zremrangebyscore(key, 0, windowStart)
20 pipeline.zadd(key, { score: now, member: `${now}-${Math.random()}` })
21 pipeline.zcard(key)
22 pipeline.expire(key, Math.ceil(windowMs / 1000))
23
24 const results = await pipeline.exec()
25 const count = results[2] as number
26
27 return {
28 success: count <= limit,
29 remaining: Math.max(0, limit - count),
30 resetIn: windowMs
31 }
32}Rate Limit Middleware#
1// middleware.ts
2import { NextResponse } from 'next/server'
3import type { NextRequest } from 'next/server'
4import { rateLimitRedis } from '@/lib/rate-limit-redis'
5
6const RATE_LIMITS = {
7 '/api/': { limit: 100, window: 60 }, // 100 req/min
8 '/api/auth/': { limit: 10, window: 60 }, // 10 req/min
9 '/api/ai/': { limit: 20, window: 60 } // 20 req/min
10}
11
12export async function middleware(request: NextRequest) {
13 const ip = request.ip ?? request.headers.get('x-forwarded-for') ?? 'unknown'
14 const path = request.nextUrl.pathname
15
16 // Find matching rate limit
17 let config = { limit: 100, window: 60 }
18 for (const [prefix, limits] of Object.entries(RATE_LIMITS)) {
19 if (path.startsWith(prefix)) {
20 config = limits
21 break
22 }
23 }
24
25 const key = `rate-limit:${ip}:${path}`
26 const result = await rateLimitRedis(key, config.limit, config.window)
27
28 if (!result.success) {
29 return NextResponse.json(
30 { error: 'Too many requests' },
31 {
32 status: 429,
33 headers: {
34 'X-RateLimit-Limit': config.limit.toString(),
35 'X-RateLimit-Remaining': '0',
36 'X-RateLimit-Reset': Math.ceil(Date.now() / 1000 + result.resetIn / 1000).toString(),
37 'Retry-After': Math.ceil(result.resetIn / 1000).toString()
38 }
39 }
40 )
41 }
42
43 const response = NextResponse.next()
44 response.headers.set('X-RateLimit-Limit', config.limit.toString())
45 response.headers.set('X-RateLimit-Remaining', result.remaining.toString())
46
47 return response
48}
49
50export const config = {
51 matcher: '/api/:path*'
52}Per-User Rate Limiting#
1// lib/rate-limit-user.ts
2import { auth } from '@/auth'
3
4export async function userRateLimit(baseLimit: number) {
5 const session = await auth()
6 const userId = session?.user?.id
7
8 // Higher limits for authenticated users
9 const limit = userId ? baseLimit * 2 : baseLimit
10
11 // Use user ID or IP as key
12 const key = userId ?? (await getClientIP())
13
14 return rateLimitRedis(`user:${key}`, limit, 60)
15}Token Bucket Algorithm#
1// lib/token-bucket.ts
2interface Bucket {
3 tokens: number
4 lastRefill: number
5}
6
7const buckets = new Map<string, Bucket>()
8
9export function tokenBucket(
10 key: string,
11 maxTokens: number,
12 refillRate: number // tokens per second
13): boolean {
14 const now = Date.now()
15 let bucket = buckets.get(key)
16
17 if (!bucket) {
18 bucket = { tokens: maxTokens, lastRefill: now }
19 buckets.set(key, bucket)
20 }
21
22 // Refill tokens
23 const timePassed = (now - bucket.lastRefill) / 1000
24 bucket.tokens = Math.min(maxTokens, bucket.tokens + timePassed * refillRate)
25 bucket.lastRefill = now
26
27 // Try to consume a token
28 if (bucket.tokens >= 1) {
29 bucket.tokens -= 1
30 return true
31 }
32
33 return false
34}Usage in Route Handler#
1// app/api/posts/route.ts
2import { NextRequest, NextResponse } from 'next/server'
3import { rateLimit } from '@/lib/rate-limit'
4
5export async function POST(request: NextRequest) {
6 const ip = request.headers.get('x-forwarded-for') ?? 'anonymous'
7
8 const result = rateLimit(ip, 10, 60000) // 10 requests per minute
9
10 if (!result.success) {
11 return NextResponse.json(
12 { error: 'Too many requests' },
13 {
14 status: 429,
15 headers: {
16 'Retry-After': Math.ceil(result.resetIn / 1000).toString()
17 }
18 }
19 )
20 }
21
22 // Continue with handler...
23}Usage Instructions#
- Choose algorithm: Select fixed window, sliding window, or token bucket based on needs
- Choose storage: In-memory for single instance, Redis for distributed
- Configure limits: Set appropriate limits per endpoint type
- Add to middleware: Implement as middleware for automatic enforcement
- Return headers: Include rate limit headers in responses
Best Practices#
- Use Redis for production - In-memory rate limiting doesn't work with multiple instances
- Set appropriate limits - Consider endpoint cost and user expectations
- Include rate limit headers - Help clients implement backoff strategies
- Differentiate by user type - Higher limits for authenticated/premium users
- Monitor and adjust - Track rate limit hits and adjust as needed
- Use sliding windows - More accurate than fixed windows for sustained traffic
- Handle gracefully - Return clear error messages with retry information
Related Patterns#
- Middleware - Request preprocessing
- Error Handling - Consistent error responses
- Caching - Reduce load with caching
- Route Handler - API endpoint implementation