Implementing Rate Limiting in Next.js 14 API Routes Using Upstash Redis and Middleware

User avatar placeholder
Written by Tamzid Ahmed

September 13, 2026

Implementing rate limiting in Next.js 14 API routes using Upstash Redis and middleware is a proven way to guard your endpoints against abuse while keeping latency low.

Understanding Rate Limiting and Why It Matters in Next.js 14 API Routes

Rate limiting controls how many requests a client can send over a given period. In a serverless environment like Vercel, uncontrolled traffic can quickly exhaust your function quotas and raise costs.

Common Rate‑Limiting Algorithms

  • Token bucket – allows bursts up to a defined capacity and then refills at a steady rate.
  • Leaky bucket – smooths traffic by processing requests at a constant rate.
  • Sliding window – offers precise per‑interval limits without burst allowance.

Prerequisites – Tools You’ll Need

Next.js 14 (app router) and Middleware

Next.js 14 introduces edge‑ready middleware that runs before a request reaches an API route. This layer is ideal for quick counters because it can execute on the edge network.

Upstash Redis (Serverless) and the @upstash/redis Package

Upstash provides a fully managed, serverless Redis instance with sub‑millisecond latency. Install the client with:

npm i @upstash/redis

Then initialize the connection in a redis.ts helper:

import { Redis } from '@upstash/redis';

export const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!
});

Setting Up Upstash Redis for a Token‑Bucket Counter

The token‑bucket pattern stores two keys per client: the remaining tokens and the timestamp of the last refill.

  1. Choose a key namespace, e.g., rl:{ip}.
  2. Define capacity (max tokens) and refill rate (tokens per second).
  3. Write a Lua script that atomically checks and updates the bucket.

Example Lua script stored in Upstash:

local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

local data = redis.call('HMGET', key, 'tokens', 'ts')
local tokens = tonumber(data[1]) or capacity
local last = tonumber(data[2]) or now

local elapsed = now - last
local newTokens = math.min(capacity, tokens + (elapsed * refill))
if newTokens < 1 then
  return 0
else
  redis.call('HMSET', key, 'tokens', newTokens - 1, 'ts', now)
  redis.call('EXPIRE', key, 3600)
  return 1
end

Creating a Reusable Rate‑Limit Middleware

Place the logic in middleware.ts so it can be imported by any API route.

import { NextResponse } from 'next/server';
import { redis } from '@/lib/redis';

const CAPACITY = 100; // requests
const REFILL_RATE = 1; // token per second

export async function rateLimiter(request: Request) {
  const ip = request.headers.get('x-forwarded-for') || 'unknown';
  const key = `rl:${ip}`;
  const now = Math.floor(Date.now() / 1000);

  const script = `YOUR_LUA_SCRIPT_HERE`;
  const allowed = await redis.eval(script, {
    keys: [key],
    args: [CAPACITY, REFILL_RATE, now]
  });

  if (!allowed) {
    return new NextResponse('Too Many Requests', { status: 429 });
  }
  return NextResponse.next();
}

Edge vs. Node Runtime Considerations

When the middleware runs on the Edge, the request latency is sub‑10 ms, but the script must be cold‑start safe. Upstash’s REST API works seamlessly from Edge locations. If you need access to the file system or other Node‑only APIs, switch the middleware to the node runtime by adding export const config = { runtime: 'nodejs' };.

Applying the Middleware to API Routes

Import the helper and invoke it at the top of your route handler.

import { rateLimiter } from '@/middleware/rateLimiter';

export async function GET(request: Request) {
  const response = await rateLimiter(request);
  if (response instanceof Response) return response; // blocked

  // Your normal logic here
  return new Response(JSON.stringify({ message: 'Success' }), { status: 200 });
}

This pattern keeps the limiter decoupled from business logic, making it reusable across dozens of endpoints.

Testing the Rate Limiter Locally and in Production

  • Run npm run dev and use curl -i http://localhost:3000/api/your-endpoint repeatedly to see a 429 after the limit.
  • Deploy to Vercel and observe the x-vercel-cache headers – they indicate Edge execution.
  • Check Upstash’s dashboard for the rl:{ip} keys to confirm token consumption.

Performance Trade‑offs and Best Practices

Even though Upstash is serverless, each check still incurs a network round‑trip. Follow these guidelines to keep latency under 20 ms:

  1. Batch reads when you need multiple counters (e.g., per‑user and per‑IP).
  2. Enable Redis key TTL to auto‑expire idle buckets and lower memory usage.
  3. Prefer the Edge runtime for public APIs; move to Node only when you need server‑side secrets.
  4. Monitor error rates – a sudden spike can indicate a denial‑of‑service attempt.
  5. Document the capacity and refill values for each endpoint to avoid accidental over‑blocking.

Conclusion

Implementing rate limiting in Next.js 14 API routes with Upstash Redis and middleware gives you a scalable, low‑latency guard against excessive traffic. By using a token‑bucket Lua script, reusable Edge middleware, and the managed Redis service, you keep the codebase clean and the performance predictable. Start protecting your next endpoint today—add the middleware, adjust the limits, and watch your API stay healthy.

Leave a Comment