Rate Limiting Next.js APIs on Vercel Edge: Stop DDoS Attacks Before They Hit Your Server

Meta description: Learn how to implement rate limiting for Next.js APIs on Vercel Edge — Upstash Redis, sliding window algorithm, bot detection, and DDoS protection in production.

Last updated: June 22, 2025


Last year, one of my Next.js APIs got hammered by a scraperbot that fired 4,000 requests per minute against a single endpoint. The app stayed up, but the database connection pool saturated within 90 seconds and legitimate users started seeing 503 errors. Vercel’s serverless functions scaled horizontally, which meant the attack kept spawning new function instances — and the cost spiked $200 in 40 minutes.

That experience taught me that rate limiting for Next.js APIs isn’t optional — and doing it at the Vercel Edge layer, before your serverless functions even execute, is the only approach that actually protects you at scale.


TL;DR

  • Implement rate limiting in Next.js Middleware — it runs at the Edge before any Lambda cold start, making it the cheapest and fastest place to block requests.
  • Use Upstash Redis with a sliding window algorithm for distributed rate limit state that works across Vercel’s global Edge network.
  • Layer IP-based throttling, bot detection headers, and Vercel’s built-in DDoS protection for defense in depth.

Why Edge-Level Rate Limiting Is the Only Approach That Scales

Traditional rate limiting runs inside your API route handlers. On a serverless platform like Vercel, this is too late — the function has already been invoked and billed before you reject the request.

Vercel Edge Middleware runs before your functions, at Cloudflare’s network layer, with sub-1ms overhead and no cold starts. That’s where rate limiting belongs.

The other challenge specific to Next.js on Vercel: you can’t use in-memory stores like express-rate-limit‘s default memory store. Each Edge function invocation is stateless, and multiple instances run in parallel across regions. You need a distributed rate limit store — and Upstash Redis is the purpose-built solution for this.

[INTERNAL LINK: related article on Next.js Middleware patterns for authentication]


Prerequisites

  • Next.js 13+ with the App Router (or Pages Router with _middleware.ts)
  • Vercel account with a deployed project
  • Upstash account (free tier works for development)
  • Basic familiarity with TypeScript

Step-by-Step: Rate Limiting and DDoS Protection on Vercel Edge

Step 1: Install Dependencies

npm install @upstash/ratelimit @upstash/redis

These two packages are specifically designed for Edge environments — no Node.js APIs, no native bindings, pure Web Standards. This is critical because Next.js Middleware runs in the Vercel Edge Runtime, not the standard Node.js runtime.

Important: Do not use ioredis or node-redis in Next.js Middleware. Both require Node.js APIs unavailable in the Edge Runtime. I spent two hours debugging a ReferenceError: process is not defined error before I found this out the hard way.


Step 2: Configure Upstash Redis

Create a Redis database in your Upstash console and copy the REST URL and token. Add them to your Vercel environment variables:

# .env.local (never commit this)
UPSTASH_REDIS_REST_URL=https://your-db.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token-here
# Set in Vercel via CLI
vercel env add UPSTASH_REDIS_REST_URL production
vercel env add UPSTASH_REDIS_REST_TOKEN production

Step 3: Create the Rate Limiter in Middleware

Create or edit middleware.ts at the root of your project (not inside /app or /pages):

import { NextRequest, NextResponse } from "next/server";
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!,
});

// Sliding window: 30 requests per 10 seconds per IP
const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(30, "10 s"),
  analytics: true, // Logs to Upstash for monitoring
  prefix: "rl:api",
});

export async function middleware(req: NextRequest) {
  // Only apply rate limiting to API routes
  if (!req.nextUrl.pathname.startsWith("/api")) {
    return NextResponse.next();
  }

  // Get the real IP (Vercel sets x-forwarded-for)
  const ip =
    req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ??
    req.headers.get("x-real-ip") ??
    "127.0.0.1";

  const { success, limit, reset, remaining } = await ratelimit.limit(ip);

  // Add rate limit headers to all responses (RFC 6585 compliance)
  const res = success
    ? NextResponse.next()
    : NextResponse.json(
        { error: "Too Many Requests", retryAfter: Math.ceil((reset - Date.now()) / 1000) },
        { status: 429 }
      );

  res.headers.set("X-RateLimit-Limit", String(limit));
  res.headers.set("X-RateLimit-Remaining", String(remaining));
  res.headers.set("X-RateLimit-Reset", String(reset));

  return res;
}

export const config = {
  matcher: ["/api/:path*"],
};

Pro Tip: The slidingWindow algorithm is more accurate than a fixed window for burst protection — it doesn’t reset the counter all at once at the end of each interval, which means a client can’t hammer your API right as a window resets. I switched from fixed window to sliding window after noticing clients were batching requests to exploit the reset timing.

[SOURCE: https://vercel.com/docs/functions/edge-middleware]


Step 4: Add Per-Route and Per-User Rate Limits

A single global IP-based limit isn’t always enough. Authenticated users should have higher limits, and expensive endpoints (like AI completions or file exports) should have tighter ones.

// Multiple rate limiters for different tiers
const limiters = {
  // Strict limit for unauthenticated IPs
  anonymous: new Ratelimit({
    redis,
    limiter: Ratelimit.slidingWindow(10, "10 s"),
    prefix: "rl:anon",
  }),
  // Higher limit for authenticated users
  authenticated: new Ratelimit({
    redis,
    limiter: Ratelimit.slidingWindow(100, "10 s"),
    prefix: "rl:auth",
  }),
  // Tight limit for expensive endpoints
  expensive: new Ratelimit({
    redis,
    limiter: Ratelimit.tokenBucket(5, "10 s", 5), // 5 tokens, refills 5/10s
    prefix: "rl:expensive",
  }),
};

export async function middleware(req: NextRequest) {
  const isExpensive = req.nextUrl.pathname.startsWith("/api/ai");
  const authToken = req.cookies.get("session")?.value;

  const identifier = authToken ? `user:${authToken}` : `ip:${getIp(req)}`;
  const limiter = isExpensive
    ? limiters.expensive
    : authToken
    ? limiters.authenticated
    : limiters.anonymous;

  const { success } = await limiter.limit(identifier);
  // ... rest of handler
}

Step 5: Layer in Bot Detection and Header-Based Protection

Rate limiting alone won’t stop all abuse. Add header-based bot signals as a second layer:

function isSuspiciousRequest(req: NextRequest): boolean {
  const ua = req.headers.get("user-agent") ?? "";
  const referer = req.headers.get("referer");

  // No user-agent on an API request is a strong bot signal
  if (!ua || ua.length < 10) return true;

  // Known scraper patterns (not exhaustive — update regularly)
  const botPatterns = /curl|wget|python-requests|scrapy|go-http|axios\/0\./i;
  if (botPatterns.test(ua)) return true;

  return false;
}

Additionally, configure Vercel’s built-in Firewall (available on Pro and Enterprise plans) with custom rules to block ranges of suspicious IPs or ASNs at the CDN layer — before middleware even runs.

# Example: block a specific CIDR range via Vercel CLI (Pro+)
vercel firewall add --action block --ip 192.0.2.0/24

[SOURCE: https://vercel.com/docs/security/vercel-firewall]


Step 6: Monitor and Tune Your Limits in Production

The biggest mistake I see teams make is setting a rate limit once and never revisiting it. Your limits need to be tuned to real traffic patterns.

Enable Upstash analytics (analytics: true in the Ratelimit config) and connect it to your Vercel dashboard. Watch the rejection rate — if it climbs above 0.5% of legitimate traffic, your limits are too tight. If it never fires at all, they may be too loose.

# Pull rate limit rejection logs from Vercel
vercel logs --filter "429" --since 24h

Real-World Tips I Use in Production

Tip 1: Use a separate Redis instance for rate limiting. Don’t share the same Upstash Redis database with your application cache. A flood attack fills your rate limit keys and could evict application cache entries if you have a shared instance with an LRU eviction policy.

Tip 2: Allowlist health check and monitoring IPs. Datadog, Pingdom, and other uptime monitors will trip your rate limits. Add their IP ranges to an allowlist at the top of your middleware, before the rate limit check.

Tip 3: Return Retry-After headers on 429 responses. Well-behaved clients and SDKs will back off automatically if you include Retry-After: <seconds>. This significantly reduces the thundering herd effect after a brief outage.


What Are the Vercel Edge Runtime Limitations for Next.js Middleware?

This is the question that trips up most developers the first time they set this up. The Edge Runtime only supports Web Standards APIs — no Node.js built-ins, no TCP clients, no native modules. Understanding this upfront saves hours of debugging. The common errors below are a direct result of ignoring it.


Common Errors and How I Fixed Them

Error: TypeError: fetch is not a function in middleware This happens when you import a package that falls back to the Node.js http module. In the Edge Runtime, only the Fetch API is available. Fix: use @upstash/redis (REST-based) instead of any TCP-based Redis client.

Error: Rate limits not being enforced — all requests pass I traced this once to a misconfigured matcher in middleware.ts. The matcher "/api/:path*" only matches paths with at least one segment after /api/. Routes like /api (no trailing segment) were skipping the middleware entirely. Fix: use ["/api", "/api/:path*"] as the matcher array.

Error: Legitimate users getting 429 behind a corporate NAT Many enterprise users share a single egress IP. Purely IP-based rate limiting will reject them. Fix: for authenticated users, switch the rate limit identifier from IP to user ID or session token, as shown in Step 4.


FAQ

Q: How do I implement rate limiting for Next.js API routes on Vercel Edge without a Redis database? A: If you want to avoid an external dependency, Vercel’s own KV store (currently in beta) works with the same @upstash/ratelimit library. For very low-traffic apps, you could also store state in Vercel KV, though Upstash is more mature and purpose-built for this use case.

Q: What is the best rate limiting algorithm for DDoS protection in Next.js? A: The sliding window algorithm (Ratelimit.slidingWindow) is the best default for API protection. It prevents burst exploitation at window boundaries. For expensive endpoints with metered access, the token bucket algorithm gives you more control over burst allowances.

Q: How do I add DDoS protection to a Next.js app deployed on Vercel? A: Layer three defenses: (1) Vercel Edge Middleware with IP-based sliding window rate limiting, (2) Vercel Firewall custom rules to block known bad actors at the CDN layer, and (3) header-based bot detection signals in your middleware. No single layer is sufficient on its own.

Q: How do I handle rate limiting for authenticated users vs. anonymous users in Next.js? A: Read the session cookie or authorization header in middleware before the rate limit check. For authenticated users, use their user ID as the rate limit identifier and apply a higher limit. For anonymous users, fall back to IP with a stricter limit. This prevents enterprise users behind shared NATs from tripping limits.

Q: What are the Vercel Edge Runtime limitations I need to know for rate limiting middleware? A: The Edge Runtime only supports the Web Standards APIs — no Node.js built-ins like net, fs, or crypto (the Node.js version). You can’t use ioredis, node-redis, or any TCP-based library. Stick to @upstash/redis (HTTP/REST-based), and avoid packages that transitively import Node.js core modules.


Conclusion

Rate limiting at the Vercel Edge is one of those infrastructure decisions that feels like over-engineering until the day it saves you. After that scraper incident, I added Edge middleware rate limiting to every Next.js project I deploy — it takes about 30 minutes to set up properly and it’s paid for itself in prevented incidents and avoided serverless costs many times over.

Start with a sliding window limiter on all /api routes, add per-user tiering for authenticated endpoints, and set up monitoring to tune your limits over time. The DDoS protection layered on top with Vercel Firewall rules handles the rest.

If you’ve run into a different Edge Runtime gotcha or have a rate limiting pattern that works well at scale, I’d love to hear about it in the comments.


💡 Gostou desse conteúdo? Acompanhe o SpiritCode e receba novidades direto no seu feed. Basta salvar o endereço spiritcode.blog nos favoritos ou nos seguir nas redes sociais — novas matérias toda semana.


About the Author

I’m a full-stack engineer with 9 years of experience building production Next.js applications, with a focus on performance, security, and edge infrastructure. My primary stack is TypeScript, Next.js, Vercel, and AWS, and I’ve shipped rate limiting and API security layers for platforms handling millions of requests per day. I write about the problems I’ve actually hit in production — not the happy path from the docs.