Rate Limiting

Patterns for implementing API rate limiting to protect against abuse.

Overview#

Rate limiting prevents abuse and ensures fair usage of your API. This pattern covers:

  • In-memory rate limiting
  • Redis-based distributed limiting
  • Sliding window algorithm
  • Token bucket algorithm
  • Per-user and per-endpoint limits

Prerequisites#

npm install @upstash/redis

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( 37 Date.now() / 1000 + result.resetIn / 1000 38 ).toString(), 39 'Retry-After': Math.ceil(result.resetIn / 1000).toString() 40 } 41 } 42 ) 43 } 44 45 const response = NextResponse.next() 46 response.headers.set('X-RateLimit-Limit', config.limit.toString()) 47 response.headers.set('X-RateLimit-Remaining', result.remaining.toString()) 48 49 return response 50} 51 52export const config = { 53 matcher: '/api/:path*' 54}

Per-User Rate Limiting#

1// lib/rate-limit-user.ts 2import { auth } from '@/auth' 3import { rateLimitRedis } from './rate-limit-redis' 4 5export async function userRateLimit(baseLimit: number) { 6 const session = await auth() 7 const userId = session?.user?.id 8 9 // Higher limits for authenticated users 10 const limit = userId ? baseLimit * 2 : baseLimit 11 12 // Use user ID or IP as key 13 const key = userId ?? (await getClientIP()) 14 15 return rateLimitRedis(`user:${key}`, limit, 60) 16} 17 18// Tiered limits based on plan 19export async function tieredRateLimit(endpoint: string) { 20 const session = await auth() 21 const plan = session?.user?.plan ?? 'free' 22 23 const limits: Record<string, number> = { 24 free: 100, 25 pro: 1000, 26 enterprise: 10000 27 } 28 29 const limit = limits[plan] ?? limits.free 30 const key = `tier:${session?.user?.id ?? 'anon'}:${endpoint}` 31 32 return rateLimitRedis(key, limit, 3600) // Per hour 33}

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 based on time passed 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} 35 36// Usage: Allow bursts but limit sustained rate 37const allowed = tokenBucket('user:123', 10, 1) // 10 burst, 1/sec sustained

API Route with Rate Limiting#

1// app/api/protected/route.ts 2import { NextRequest, NextResponse } from 'next/server' 3import { rateLimitRedis } from '@/lib/rate-limit-redis' 4 5export async function POST(request: NextRequest) { 6 const ip = request.ip ?? 'unknown' 7 8 const { success, remaining, resetIn } = await rateLimitRedis( 9 `api:protected:${ip}`, 10 10, // 10 requests 11 60 // per minute 12 ) 13 14 if (!success) { 15 return NextResponse.json( 16 { error: 'Rate limit exceeded. Please try again later.' }, 17 { 18 status: 429, 19 headers: { 20 'Retry-After': Math.ceil(resetIn / 1000).toString() 21 } 22 } 23 ) 24 } 25 26 // Process request... 27 const data = await request.json() 28 29 return NextResponse.json( 30 { success: true }, 31 { 32 headers: { 33 'X-RateLimit-Remaining': remaining.toString() 34 } 35 } 36 ) 37}

Usage Instructions#

  1. Choose the appropriate algorithm for your use case
  2. Use Redis for distributed rate limiting across multiple instances
  3. Set different limits for different endpoints and user tiers
  4. Include rate limit headers in responses
  5. Return 429 status with Retry-After header when limited

Best Practices#

  • Use Redis in production - In-memory only works for single instances
  • Different limits per endpoint - Auth endpoints need stricter limits
  • Higher limits for authenticated users - Reward logged-in users
  • Include rate limit headers - Help clients manage their requests
  • Log rate limit hits - Monitor for abuse patterns
  • Graceful degradation - If Redis fails, allow requests through
  • Consider sliding windows - More accurate than fixed windows