Rate Limiting for Next.js API Routes
How to implement rate limiting in Next.js API Routes and Route Handlers to protect against abuse
Rate Limiting for Next.js API Routes
Introduction
Rate limiting is a fundamental security measure for any API. Without it, a single malicious actor can exhaust your server resources, increase your hosting costs, or perform denial-of-service attacks. Next.js API Routes and Route Handlers do not include rate limiting out of the box.
After a bot crawled my API endpoints and generated a $2,000 hosting bill in a single night, I implemented a comprehensive rate limiting strategy. This post covers three approaches from simple to production-ready.
Environment
- OS: Windows 11
- Node.js: v20.10.0
- Next.js: 14.0.4
- Rate Limiter: Upstash Redis 1.25.0
Problem
Without rate limiting, the API was vulnerable:
# Bot making 1000 requests per second
for i in {1..10000}; do
curl -s https://api.example.com/data &
done
# Server response time degraded from 50ms to 5000ms
# Memory usage spiked from 200MB to 2GB
# Vercel function timeout errors appearedThe API route had no protection:
// app/api/data/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
const data = await expensiveDatabaseQuery()
return NextResponse.json(data)
}Solution
Approach 1: Simple in-memory rate limiter
// lib/rate-limit.ts
interface RateLimitEntry {
count: number
resetTime: number
}
const rateLimitMap = new Map()
interface RateLimitConfig {
windowMs: number // Time window in milliseconds
maxRequests: number // Max requests per window
}
export function rateLimit(
key: string,
config: RateLimitConfig
): { success: boolean; remaining: number; resetTime: number } {
const { windowMs, maxRequests } = config
const now = Date.now()
const entry = rateLimitMap.get(key)
if (!entry || now > entry.resetTime) {
// New window
rateLimitMap.set(key, {
count: 1,
resetTime: now + windowMs,
})
return { success: true, remaining: maxRequests - 1, resetTime: now + windowMs }
}
if (entry.count >= maxRequests) {
// Rate limit exceeded
return { success: false, remaining: 0, resetTime: entry.resetTime }
}
// Increment counter
entry.count++
return { success: true, remaining: maxRequests - entry.count, resetTime: entry.resetTime }
}
// Clean up expired entries periodically
setInterval(() => {
const now = Date.now()
for (const [key, entry] of rateLimitMap) {
if (now > entry.resetTime) {
rateLimitMap.delete(key)
}
}
}, 60000) // Clean up every minute // app/api/data/route.ts
import { NextResponse } from 'next/server'
import { rateLimit } from '@/lib/rate-limit'
export async function GET(request: Request) {
const ip = request.headers.get('x-forwarded-for') || 'anonymous'
const { success, remaining, resetTime } = rateLimit(ip, {
windowMs: 60 * 1000, // 1 minute
maxRequests: 60, // 60 requests per minute
})
if (!success) {
return NextResponse.json(
{ error: 'Rate limit exceeded' },
{
status: 429,
headers: {
'X-RateLimit-Limit': '60',
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': String(Math.ceil(resetTime / 1000)),
'Retry-After': String(Math.ceil((resetTime - Date.now()) / 1000)),
},
}
)
}
const data = await expensiveDatabaseQuery()
return NextResponse.json(data, {
headers: {
'X-RateLimit-Limit': '60',
'X-RateLimit-Remaining': String(remaining),
'X-RateLimit-Reset': String(Math.ceil(resetTime / 1000)),
},
})
}Approach 2: Upstash Redis rate limiter (production)
// lib/rate-limit-redis.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})
// Sliding window rate limiter
export const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(60, '60 s'), // 60 requests per 60 seconds
analytics: true,
prefix: 'upstash_ratelimit',
})
// Different limits for different endpoints
export const apiLimiter = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(100, '60 s'),
analytics: true,
prefix: 'api_limit',
})
export const strictLimiter = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(5, '60 s'), // 5 requests per minute
analytics: true,
prefix: 'strict_limit',
})// app/api/auth/login/route.ts
import { NextResponse } from 'next/server'
import { strictLimiter } from '@/lib/rate-limit-redis'
export async function POST(request: Request) {
const ip = request.headers.get('x-forwarded-for') || 'anonymous'
const { success, remaining, reset } = await strictLimiter.limit(ip)
if (!success) {
return NextResponse.json(
{ error: 'Too many login attempts' },
{
status: 429,
headers: {
'X-RateLimit-Remaining': String(remaining),
'X-RateLimit-Reset': String(reset),
},
}
)
}
// Process login...
return NextResponse.json({ success: true })
}Approach 3: Middleware-based rate limiting
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(100, '60 s'),
})
export async function middleware(request: NextRequest) {
// Only rate limit API routes
if (!request.nextUrl.pathname.startsWith('/api')) {
return NextResponse.next()
}
const ip = request.headers.get('x-forwarded-for') || 'anonymous'
const { success, remaining, reset } = await ratelimit.limit(ip)
if (!success) {
return NextResponse.json(
{ error: 'Rate limit exceeded' },
{
status: 429,
headers: {
'X-RateLimit-Limit': '100',
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': String(Math.ceil(reset / 1000)),
},
}
)
}
const response = NextResponse.next()
response.headers.set('X-RateLimit-Remaining', String(remaining))
return response
}
export const config = {
matcher: ['/api/:path*'],
}Lessons Learned
In-memory rate limiting is for development only. It does not work across multiple server instances and loses state on restart.
Use Upstash Redis for production. It provides distributed rate limiting with minimal setup and works perfectly with Vercel.
Rate limit by IP and API key separately. Authenticated users should have higher limits than anonymous users.
Always return rate limit headers. Clients can use
X-RateLimit-Remainingto implement client-side throttling.Combine multiple strategies. Use middleware for global rate limiting and route-level limiting for sensitive endpoints like authentication.
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.