Rate limiting protects your API from abuse, prevents DDoS attacks, and manages server costs. Here are several approaches for Next.js.
Step 1: Simple In-Memory Rate Limiter
Good for single-server deployments:
// lib/rate-limit.ts
const requests = new Map<string, { count: number; resetTime: number }>();
export function rateLimit(
ip: string,
limit: number = 10,
windowMs: number = 60_000
): { success: boolean; remaining: number; resetIn: number } {
const now = Date.now();
const record = requests.get(ip);
if (!record || now > record.resetTime) {
requests.set(ip, { count: 1, resetTime: now + windowMs });
return { success: true, remaining: limit - 1, resetIn: windowMs };
}
if (record.count >= limit) {
return {
success: false,
remaining: 0,
resetIn: record.resetTime - now,
};
}
record.count++;
return {
success: true,
remaining: limit - record.count,
resetIn: record.resetTime - now,
};
}
// Cleanup stale entries every 5 minutes
setInterval(() => {
const now = Date.now();
for (const [ip, record] of requests) {
if (now > record.resetTime) requests.delete(ip);
}
}, 300_000);
Step 2: Use in API Route
// app/api/contact/route.ts
import { NextRequest, NextResponse } from "next/server";
import { rateLimit } from "@/lib/rate-limit";
export async function POST(req: NextRequest) {
const ip = req.headers.get("x-forwarded-for") ?? "unknown";
const { success, remaining, resetIn } = rateLimit(ip, 5, 60_000);
if (!success) {
return NextResponse.json(
{ error: "Too many requests. Please try again later." },
{
status: 429,
headers: {
"Retry-After": String(Math.ceil(resetIn / 1000)),
"X-RateLimit-Remaining": "0",
},
}
);
}
// Process the request
const body = await req.json();
// ... handle form submission
return NextResponse.json(
{ success: true },
{
headers: {
"X-RateLimit-Remaining": String(remaining),
},
}
);
}
Step 3: Redis-Backed Rate Limiter
For multi-server deployments, use Redis:
pnpm add @upstash/ratelimit @upstash/redis
// lib/rate-limit.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
export const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "60 s"),
analytics: true,
prefix: "api",
});
// app/api/contact/route.ts
import { NextRequest, NextResponse } from "next/server";
import { ratelimit } from "@/lib/rate-limit";
export async function POST(req: NextRequest) {
const ip = req.headers.get("x-forwarded-for") ?? "unknown";
const { success, limit, remaining, reset } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: "Too many requests" },
{
status: 429,
headers: {
"X-RateLimit-Limit": String(limit),
"X-RateLimit-Remaining": String(remaining),
"X-RateLimit-Reset": String(reset),
},
}
);
}
// Process request...
return NextResponse.json({ success: true });
}
Step 4: Middleware-Based Rate Limiting
Apply rate limiting globally via middleware:
// 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.fixedWindow(100, "60 s"),
});
export async function middleware(req: NextRequest) {
// Only rate limit API routes
if (!req.nextUrl.pathname.startsWith("/api/")) {
return NextResponse.next();
}
const ip = req.headers.get("x-forwarded-for") ?? "127.0.0.1";
const { success, limit, remaining } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: "Rate limit exceeded" },
{ status: 429 }
);
}
const response = NextResponse.next();
response.headers.set("X-RateLimit-Limit", String(limit));
response.headers.set("X-RateLimit-Remaining", String(remaining));
return response;
}
export const config = {
matcher: "/api/:path*",
};
Step 5: Per-Route Rate Limits
// lib/rate-limit.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
export const rateLimiters = {
// Contact form: 5 per minute
contact: new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(5, "60 s"),
prefix: "api:contact",
}),
// Auth: 10 per minute
auth: new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, "60 s"),
prefix: "api:auth",
}),
// General API: 100 per minute
api: new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(100, "60 s"),
prefix: "api:general",
}),
};
Step 6: Rate Limit Helper
// lib/with-rate-limit.ts
import { NextRequest, NextResponse } from "next/server";
import { rateLimiters } from "./rate-limit";
type RateLimitKey = keyof typeof rateLimiters;
export function withRateLimit(
handler: (req: NextRequest) => Promise<NextResponse>,
key: RateLimitKey = "api"
) {
return async (req: NextRequest) => {
const ip = req.headers.get("x-forwarded-for") ?? "unknown";
const { success, remaining } = await rateLimiters[key].limit(ip);
if (!success) {
return NextResponse.json(
{ error: "Too many requests" },
{ status: 429 }
);
}
const response = await handler(req);
response.headers.set("X-RateLimit-Remaining", String(remaining));
return response;
};
}
// Usage
export const POST = withRateLimit(async (req) => {
const body = await req.json();
// ... process request
return NextResponse.json({ success: true });
}, "contact");
Step 7: Client-Side Handling
"use client";
async function handleSubmit(data: FormData) {
const res = await fetch("/api/contact", {
method: "POST",
body: JSON.stringify(data),
});
if (res.status === 429) {
const retryAfter = res.headers.get("Retry-After");
toast.error(
`Too many requests. Please try again in ${retryAfter} seconds.`
);
return;
}
if (!res.ok) {
toast.error("Something went wrong");
return;
}
toast.success("Message sent!");
}
Need Secure API Architecture?
We build web applications with proper security measures including rate limiting, authentication, and input validation. Contact us to discuss your project.