How to Implement Rate Limiting
Stop abuse before it stops your server
TL;DR
TL;DR
Use Upstash Rate Limit for serverless apps or express-rate-limit for traditional Node.js. Track requests by IP or user ID. Return 429 status when limits are exceeded. Start with conservative limits (100 requests/minute) and adjust based on real usage.
Why Rate Limiting Matters
Without rate limiting, a single user or bot can:
- Overwhelm your server with requests
- Run up your API costs (OpenAI, Stripe, etc.)
- Brute force passwords or API keys
- Scrape your entire database
Option 1: Upstash Rate Limit (Serverless)
Best for Vercel, Netlify, and other serverless platforms.
Create a rate limiter
// lib/ratelimit.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!,
});
// 10 requests per 10 seconds
export const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, '10 s'),
analytics: true,
});
Use in your API route
// app/api/protected/route.ts
import { ratelimit } from '@/lib/ratelimit';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
// Get identifier (IP or user ID)
const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1';
// Check rate limit
const { success, limit, reset, remaining } = await ratelimit.limit(ip);
if (!success) {
return Response.json(
{ error: 'Too many requests' },
{
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
}
}
);
}
// Process the request
return Response.json({ message: 'Success' });
}
Option 2: Express Rate Limit (Traditional)
Best for Express.js and traditional Node.js servers.
Create rate limit middleware
import rateLimit from 'express-rate-limit';
// General API limiter
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: { error: 'Too many requests, try again later' },
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false,
});
// Stricter limiter for auth routes
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 attempts per hour
message: { error: 'Too many login attempts' },
});
// Apply to routes
app.use('/api/', apiLimiter);
app.use('/api/auth/', authLimiter);
Option 3: Next.js Middleware
Apply rate limiting to all routes using middleware.
// middleware.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
import { NextRequest, NextResponse } from 'next/server';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(20, '10 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') ?? '127.0.0.1';
const { success } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: 'Rate limit exceeded' },
{ status: 429 }
);
}
return NextResponse.next();
}
export const config = {
matcher: '/api/:path*',
};
Rate Limiting Strategies
Fixed Window
Simple but can allow bursts at window boundaries. Example: 100 requests per minute, resets at the start of each minute.
Sliding Window
Smoother distribution, recommended for most use cases. Looks at the last N seconds continuously.
Token Bucket
Allows controlled bursts while maintaining average rate. Good for APIs that need occasional spikes.
// Different strategies with Upstash
import { Ratelimit } from '@upstash/ratelimit';
// Fixed window: 10 requests per 10 seconds
Ratelimit.fixedWindow(10, '10 s')
// Sliding window: smoother, recommended
Ratelimit.slidingWindow(10, '10 s')
// Token bucket: allows bursts
Ratelimit.tokenBucket(10, '10 s', 5) // 5 tokens max burst
Choosing What to Limit By
IP Address
// Most common, but can affect shared IPs
const ip = request.headers.get('x-forwarded-for') ??
request.headers.get('x-real-ip') ??
'127.0.0.1';
const identifier = ip.split(',')[0].trim();
User ID (Authenticated)
// Better for logged-in users
const session = await getSession(request);
const identifier = session?.user?.id ?? ip;
API Key
// For public APIs
const apiKey = request.headers.get('x-api-key');
const identifier = apiKey ?? ip;
Tip: Different Limits for Different Routes
Apply stricter limits to sensitive endpoints like login, password reset, and payment processing. Be more generous with read-only endpoints.
Recommended Limits
- General API: 100 requests/minute
- Login attempts: 5 per hour per IP
- Password reset: 3 per hour per email
- File uploads: 10 per hour
- AI/LLM calls: 20 per minute (cost protection)
- Public read API: 1000 per minute
Handling Rate Limit Errors
// Client-side handling
async function fetchWithRetry(url: string, options?: RequestInit) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : 60000;
// Show user-friendly message
throw new Error(`Rate limited. Please wait ${Math.ceil(waitTime / 1000)} seconds.`);
}
return response;
}
Testing Rate Limits
# Bash: Send many requests quickly
for i in {1..20}; do
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/api/test
done
# You should see 200s then 429s