Introduction
Without rate limiting, a single user—or bot—can hammer your server with thousands of requests per second. Proxy is the perfect place to implement rate limiting because it catches abuse before your route handlers even execute.
Key Concepts
Rate limiting restricts how many requests a client can make in a given time window. Common strategies:
- Fixed window: 100 requests per minute, resets at minute boundaries
- Sliding window: 100 requests in any 60-second period
- Token bucket: Refills tokens over time, burst-friendly
Real World Context
Rate limiting protects against:
- Brute force attacks on login endpoints
- API abuse from aggressive scrapers
- Accidental denial of service from buggy clients
- Cost overruns from excessive API calls
Deep Dive
Simple In-Memory Rate Limiter
For single-instance deployments:
typescriptimport { NextRequest, NextResponse } from 'next/server'; // Simple in-memory store (use Redis in production) const rateLimitMap = new Map<string, { count: number; timestamp: number }>(); const RATE_LIMIT = 100; // requests const WINDOW_MS = 60 * 1000; // 1 minute function getRateLimitKey(request: NextRequest): string { // Use IP address as identifier const ip = request.ip || request.headers.get('x-forwarded-for') || 'unknown'; return ip; } export function proxy(request: NextRequest) { const key = getRateLimitKey(request); const now = Date.now(); const record = rateLimitMap.get(key); if (!record || now - record.timestamp > WINDOW_MS) { // New window rateLimitMap.set(key, { count: 1, timestamp: now }); } else if (record.count >= RATE_LIMIT) { // Rate limit exceeded return NextResponse.json( { error: 'Too many requests. Please try again later.' }, { status: 429, headers: { 'Retry-After': String(Math.ceil((record.timestamp + WINDOW_MS - now) / 1000)), }, } ); } else { // Increment counter record.count++; } return NextResponse.next(); }
Per-Endpoint Rate Limits
Different endpoints need different limits:
typescriptconst LIMITS = { '/api/login': { limit: 5, windowMs: 60000 }, // 5 per minute '/api/register': { limit: 3, windowMs: 60000 }, // 3 per minute '/api/search': { limit: 30, windowMs: 60000 }, // 30 per minute default: { limit: 100, windowMs: 60000 }, // 100 per minute }; function getLimitConfig(pathname: string) { for (const [path, config] of Object.entries(LIMITS)) { if (path !== 'default' && pathname.startsWith(path)) { return config; } } return LIMITS.default; }
Redis-Based Rate Limiting (Production)
typescriptimport { Redis } from '@upstash/redis'; const redis = new Redis({ url: process.env.UPSTASH_REDIS_URL!, token: process.env.UPSTASH_REDIS_TOKEN!, }); export async function proxy(request: NextRequest) { const ip = request.ip || 'unknown'; const key = `ratelimit:${ip}`; const [count] = await redis .multi() .incr(key) .expire(key, 60) // 60 second window .exec(); if ((count as number) > 100) { return NextResponse.json( { error: 'Rate limit exceeded' }, { status: 429 } ); } return NextResponse.next(); }
Adding Rate Limit Headers
typescriptconst response = NextResponse.next(); response.headers.set('X-RateLimit-Limit', String(RATE_LIMIT)); response.headers.set('X-RateLimit-Remaining', String(RATE_LIMIT - count)); response.headers.set('X-RateLimit-Reset', String(resetTimestamp)); return response;
Common Pitfalls
-
In-memory limits don't scale: With multiple server instances, each has its own counter. Use Redis for distributed deployments.
-
IP-based limits block shared IPs: Corporate networks and VPNs share IPs. Consider user-based limits for authenticated routes.
-
Forgetting the Retry-After header: Clients need to know when they can retry. Include this header in 429 responses.
Best Practices
- Use Redis for production: In-memory stores don't work with multiple instances
- Different limits for different endpoints: Login needs stricter limits than read endpoints
- Include rate limit headers: Help good-faith clients stay within limits
- Monitor and alert: Track when users hit rate limits—it could indicate attacks or bugs
Summary
Rate limiting in Proxy protects your application at the network edge. Use IP-based identification, implement per-endpoint limits, and always include proper headers. For production, use Redis or a dedicated rate limiting service like Upstash.