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/redisCode 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 sustainedAPI 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#
- Choose the appropriate algorithm for your use case
- Use Redis for distributed rate limiting across multiple instances
- Set different limits for different endpoints and user tiers
- Include rate limit headers in responses
- 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
Related Patterns#
- Security Headers - HTTP security headers
- API Middleware - Request middleware
- Monitoring - Track rate limit metrics
- Caching - Reduce load on endpoints