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#

  1. Choose algorithm: Select fixed window, sliding window, or token bucket based on needs
  2. Choose storage: In-memory for single instance, Redis for distributed
  3. Configure limits: Set appropriate limits per endpoint type
  4. Add to middleware: Implement as middleware for automatic enforcement
  5. Return headers: Include rate limit headers in responses

Best Practices#

  1. Use Redis for production - In-memory rate limiting doesn't work with multiple instances
  2. Set appropriate limits - Consider endpoint cost and user expectations
  3. Include rate limit headers - Help clients implement backoff strategies
  4. Differentiate by user type - Higher limits for authenticated/premium users
  5. Monitor and adjust - Track rate limit hits and adjust as needed
  6. Use sliding windows - More accurate than fixed windows for sustained traffic
  7. Handle gracefully - Return clear error messages with retry information