Skip to main content
Back to Blog
Tutorials
3 min read
November 9, 2024

How to Implement Rate Limiting in Next.js API Routes

Protect your Next.js API routes with rate limiting. In-memory, Redis-backed, and middleware-based approaches.

Ryel Banfield

Founder & Lead Developer

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.

rate limitingAPINext.jssecuritytutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles