Rate Limiting
Protect your API from abuse with rate limiting
Rate Limiting
Implement rate limiting to prevent abuse and protect your infrastructure.
Why Rate Limiting?
- Prevent DDoS attacks
- Protect against brute force attacks
- Control API costs
- Ensure fair usage
- Improve system stability
Upstash Redis
Use Upstash for serverless-friendly rate limiting.
Setup
pnpm add @upstash/ratelimit @upstash/redisConfigure environment variables:
UPSTASH_REDIS_REST_URL=https://...
UPSTASH_REDIS_REST_TOKEN=...Basic Implementation
// lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const redis = Redis.fromEnv();
export const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, '10 s'),
analytics: true,
});API Route Protection
// app/api/protected/route.ts
import { ratelimit } from '@/lib/rate-limit';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const ip = request.headers.get('x-forwarded-for') ?? 'anonymous';
const { success, limit, remaining, reset } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{
error: 'Too many requests',
limit,
remaining,
reset,
},
{
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
},
}
);
}
// Process request
return NextResponse.json({ success: true });
}Different Strategies
Fixed Window
export const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.fixedWindow(10, '60 s'),
});Sliding Window
export const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, '60 s'),
});Token Bucket
export const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.tokenBucket(10, '60 s', 100),
});Per-User Rate Limiting
import { auth } from '@/lib/auth';
export async function POST(request: Request) {
const session = await auth();
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const identifier = `user:${session.user.id}`;
const { success } = await ratelimit.limit(identifier);
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
);
}
// Process request
}Different Limits for Different Tiers
// lib/rate-limit.ts
export const freeTierLimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, '1 h'),
});
export const proTierLimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(100, '1 h'),
});
export const enterpriseLimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(1000, '1 h'),
});export async function POST(request: Request) {
const session = await auth();
const user = await db.user.findUnique({
where: { id: session.user.id },
});
let limiter;
switch (user?.tier) {
case 'pro':
limiter = proTierLimit;
break;
case 'enterprise':
limiter = enterpriseLimit;
break;
default:
limiter = freeTierLimit;
}
const { success } = await limiter.limit(`user:${user.id}`);
if (!success) {
return NextResponse.json(
{ error: 'Rate limit exceeded for your tier' },
{ status: 429 }
);
}
// Process request
}Middleware Rate Limiting
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { ratelimit } from '@/lib/rate-limit';
export async function middleware(request: NextRequest) {
const ip = request.ip ?? 'anonymous';
// Only rate limit API routes
if (request.nextUrl.pathname.startsWith('/api/')) {
const { success } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
);
}
}
return NextResponse.next();
}
export const config = {
matcher: '/api/:path*',
};Magic Link Rate Limiting
Prevent magic link spam:
// app/api/auth/magic-link/route.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const magicLinkLimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(3, '1 h'),
});
export async function POST(request: Request) {
const { email } = await request.json();
const { success } = await magicLinkLimit.limit(email);
if (!success) {
return NextResponse.json(
{
error: 'Too many magic link requests. Please try again later.',
},
{ status: 429 }
);
}
// Send magic link
}Password Reset Rate Limiting
const passwordResetLimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(3, '1 h'),
});
export async function POST(request: Request) {
const { email } = await request.json();
const { success } = await passwordResetLimit.limit(email);
if (!success) {
// Don't reveal if rate limit hit for security
return NextResponse.json({
message: 'If an account exists, a reset link will be sent.',
});
}
// Send password reset email
}Error Response Format
interface RateLimitError {
error: string;
limit: number;
remaining: number;
reset: number; // Unix timestamp
}
// Client can use reset time to show countdown
const secondsUntilReset = Math.ceil((reset - Date.now()) / 1000);Client-Side Handling
'use client';
import { useState } from 'react';
export function RateLimitedForm() {
const [error, setError] = useState<string | null>(null);
const [retryAfter, setRetryAfter] = useState<number | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const response = await fetch('/api/endpoint', {
method: 'POST',
body: JSON.stringify(data),
});
if (response.status === 429) {
const data = await response.json();
const secondsUntilReset = Math.ceil((data.reset - Date.now()) / 1000);
setRetryAfter(secondsUntilReset);
setError(`Too many requests. Try again in ${secondsUntilReset} seconds.`);
return;
}
// Handle success
} catch (error) {
console.error('Error:', error);
}
};
return (
<form onSubmit={handleSubmit}>
{error && <div className="text-red-500">{error}</div>}
{/* Form fields */}
<button type="submit" disabled={retryAfter !== null}>
Submit
</button>
</form>
);
}Monitoring
Track rate limit hits:
import * as Sentry from '@sentry/nextjs';
const { success, limit, remaining } = await ratelimit.limit(identifier);
if (!success) {
Sentry.captureMessage('Rate limit exceeded', {
level: 'warning',
extra: {
identifier,
limit,
remaining,
},
});
}Best Practices
- Different limits for different endpoints: Critical endpoints need stricter limits
- User-based limits: More generous for authenticated users
- Clear error messages: Tell users when they can retry
- Return rate limit headers: Help clients implement backoff
- Monitor and adjust: Review limits based on actual usage
- Whitelist trusted IPs: Allow internal services
- Use sliding windows: Smoother rate limiting than fixed windows
Next Steps
- Configure Security Headers
- Implement Input Validation
- Learn about Error Handling