GetLaunchpad
Back to blog

Rate limiting Next.js API routes with Upstash Redis

Without rate limiting, your Next.js API routes are open to abuse — anyone can hammer your Stripe checkout endpoint and create thousands of orphaned sessions. Here's how to add sliding window rate limiting with Upstash Redis in under 30 lines.

Without rate limiting, your Next.js API routes are open to abuse — anyone can hammer your Stripe checkout endpoint and create thousands of orphaned sessions, enumerate user accounts through your auth endpoints, or burn through your OpenAI credits in minutes. Here's how to add sliding window rate limiting with Upstash Redis in under 30 lines, using the same pattern deployed in GetLaunchpad.

Why rate limiting matters for SaaS APIs

Three attack surfaces appear in almost every SaaS product:

Install the Upstash packages

npm install @upstash/redis @upstash/ratelimit

Create a Redis database in the Upstash console and copy the REST URL and token into your environment:

UPSTASH_REDIS_REST_URL=https://your-db.upstash.io
UPSTASH_REDIS_REST_TOKEN=your_token_here

Create the Redis client

// lib/redis.ts
import "server-only";
import { Redis } from "@upstash/redis";

export const redis = Redis.fromEnv();

Redis.fromEnv() reads UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN automatically. The server-only import prevents this file from being bundled into client components, keeping your Redis token out of the browser.

Sliding window vs fixed window

Upstash Ratelimit offers three algorithms. Use sliding window for API rate limiting:

Implement the rate limiter

// lib/ratelimit.ts
import "server-only";
import { Ratelimit } from "@upstash/ratelimit";
import { redis } from "./redis";

// 5 requests per 60 seconds per identifier
export const checkoutRatelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(5, "60 s"),
  analytics: true, // records metrics in Upstash console
});

Apply rate limiting to your checkout route

// app/api/stripe/checkout/route.ts
import { auth } from "@clerk/nextjs/server";
import { NextRequest, NextResponse } from "next/server";
import { checkoutRatelimit } from "@/lib/ratelimit";

export async function POST(request: NextRequest) {
  const { userId } = await auth();
  if (!userId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  // Use the authenticated user ID as the rate limit key
  const { success, limit, remaining, reset } = await checkoutRatelimit.limit(userId);

  if (!success) {
    return NextResponse.json(
      { error: "Too many requests" },
      {
        status: 429,
        headers: {
          "X-RateLimit-Limit": limit.toString(),
          "X-RateLimit-Remaining": remaining.toString(),
          "Retry-After": Math.ceil((reset - Date.now()) / 1000).toString(),
        },
      },
    );
  }

  // ... create Stripe Checkout Session
}

Using the authenticated userId as the key means each user has their own counter. An unauthenticated request is rejected at the auth check before the rate limiter is consulted — which also saves a Redis round-trip.

Return a proper 429 with Retry-After

The Retry-Afterheader tells clients how many seconds to wait before retrying. RFC 6585 defines this as a standard header for 429 responses. Well-behaved API clients and SDKs will back off automatically if you include it — and it gives your frontend the information it needs to show a user-friendly "Please wait X seconds" message rather than a generic error.

The reset value from Upstash is a Unix timestamp in milliseconds. Subtract the current time and divide by 1000 to get seconds, then ceil to avoid returning zero.

Testing locally

Upstash Redis works over HTTP, so the same client that runs in production also works on your local machine — no Docker, no local Redis installation required. Set the env vars in .env.local and run your dev server:

npm run dev
# Then in another terminal, hammer the endpoint:
for i in {1..10}; do curl -X POST http://localhost:3000/api/stripe/checkout; done

After the 5th request within 60 seconds you should see a 429 response with the Retry-Afterheader set. The Upstash console's analytics tab will show the request counters updating in near-real-time.


The complete rate limiting setup — Upstash Redis client, sliding window limiter, and the 429 handler with Retry-After — is pre-wired in GetLaunchpad, a Next.js 16 SaaS boilerplate. Your checkout endpoint ships protected on day one.

More articles