Rate Limiting

intermediate

API rate limiting with sliding window algorithm using Upstash Redis.

apirate-limitingredissecurity
Tested on201619TS5.9
$ bunx sinew add api/rate-limiting

1The Problem

Without rate limiting, your API is vulnerable to:

  • Denial of service attacks
  • Credential stuffing
  • Resource exhaustion
  • Excessive costs from abuse

2The Solution

Use Upstash Rate Limit with a sliding window algorithm for fair, consistent limiting.

3Files

lib/rate-limit.ts

lib/rate-limit.tsTypeScript
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

// Create a new ratelimiter that allows 10 requests per 10 seconds
export const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, "10 s"),
  analytics: true,
  prefix: "@upstash/ratelimit",
});

// Stricter limit for auth endpoints
export const authRatelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(5, "1 m"),
  analytics: true,
  prefix: "@upstash/ratelimit/auth",
});

// Generous limit for read-only endpoints
export const readRatelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(100, "10 s"),
  analytics: true,
  prefix: "@upstash/ratelimit/read",
});

lib/rate-limit-middleware.ts

lib/rate-limit-middleware.tsTypeScript
import { NextResponse } from "next/server";
import { ratelimit } from "./rate-limit";

export async function withRateLimit(request: Request, handler: () => Promise<Response>) {
  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" },
      {
        status: 429,
        headers: {
          "X-RateLimit-Limit": limit.toString(),
          "X-RateLimit-Remaining": remaining.toString(),
          "X-RateLimit-Reset": reset.toString(),
          "Retry-After": Math.ceil((reset - Date.now()) / 1000).toString(),
        },
      }
    );
  }

  const response = await handler();

  // Clone response to add headers
  const newResponse = new NextResponse(response.body, response);
  newResponse.headers.set("X-RateLimit-Limit", limit.toString());
  newResponse.headers.set("X-RateLimit-Remaining", remaining.toString());
  newResponse.headers.set("X-RateLimit-Reset", reset.toString());

  return newResponse;
}

app/api/example/route.ts

app/api/example/route.tsTypeScript
import { NextResponse } from "next/server";
import { ratelimit } from "@/lib/rate-limit";

export async function GET(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. Please try again later." },
      {
        status: 429,
        headers: {
          "Retry-After": Math.ceil((reset - Date.now()) / 1000).toString(),
        },
      }
    );
  }

  return NextResponse.json(
    { message: "Hello, world!" },
    {
      headers: {
        "X-RateLimit-Limit": limit.toString(),
        "X-RateLimit-Remaining": remaining.toString(),
      },
    }
  );
}

middleware.ts

middleware.tsTypeScript
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(20, "10 s"),
});

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

  const ip = request.ip ?? "127.0.0.1";
  const { success, limit, remaining } = await ratelimit.limit(ip);

  if (!success) {
    return NextResponse.json({ error: "Too many requests" }, { status: 429 });
  }

  const response = NextResponse.next();
  response.headers.set("X-RateLimit-Limit", limit.toString());
  response.headers.set("X-RateLimit-Remaining", remaining.toString());
  return response;
}

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

.env.example

.env.exampleBash
UPSTASH_REDIS_REST_URL="https://..."
UPSTASH_REDIS_REST_TOKEN="..."

4Dependencies

$ bun add @upstash/ratelimit @upstash/redis

5Configuration

Rate Limit Algorithms

// Fixed window - resets at fixed intervals
Ratelimit.fixedWindow(10, "1 m");

// Sliding window - smoother limiting
Ratelimit.slidingWindow(10, "10 s");

// Token bucket - allows bursts
Ratelimit.tokenBucket(10, "1 s", 20);
TypeScript

Custom Identifiers

// Rate limit by user ID instead of IP
const userId = session?.user?.id ?? "anonymous";
const { success } = await ratelimit.limit(userId);

// Rate limit by API key
const apiKey = request.headers.get("x-api-key") ?? "anonymous";
const { success } = await ratelimit.limit(`api:${apiKey}`);
TypeScript

6Usage

Different Limits for Different Endpoints

// Strict for auth
const authLimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(5, "15 m"), // 5 attempts per 15 min
});

// Generous for reads
const readLimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(1000, "1 h"),
});

// Moderate for writes
const writeLimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(100, "1 h"),
});
TypeScript

7Troubleshooting

Rate limiting not working

  • Verify Upstash credentials are correct
  • Check that the Redis instance is accessible
  • Ensure the identifier (IP/user ID) is being extracted correctly

Too aggressive limiting

  • Adjust the window size and request count
  • Consider using token bucket for burst tolerance
  • Use different limits for different endpoint types

Related patterns