Skip to main content

What you’ll build

A Next.js API route that limits each user to a set number of requests per time window. Excess requests get rejected with a 429. Time to complete: ~5 minutes

Prerequisites

1

Create a Next.js app

Skip if you have an existing project.
npx create-next-app@latest my-app
cd my-app
2

Install the SDK

npm install @unkey/ratelimit
3

Add your root key

Create or update .env.local:
.env.local
UNKEY_ROOT_KEY="unkey_..."
Never commit your root key. Add .env.local to .gitignore.
4

Create a rate-limited route

app/api/protected/route.ts
import { NextResponse } from "next/server";
import { Ratelimit } from "@unkey/ratelimit";

// Create limiter instance outside the handler
const limiter = new Ratelimit({
  rootKey: process.env.UNKEY_ROOT_KEY!,
  namespace: "my-app",  // Group related limits
  limit: 10,            // 10 requests...
  duration: "60s",      // ...per minute
});

export async function POST(req: Request) {
  // 1. Identify the user (IP, user ID, API key, etc.)
  const identifier = req.headers.get("x-user-id") 
    ?? req.headers.get("x-forwarded-for") 
    ?? "anonymous";

  // 2. Check the rate limit
  const { success, remaining, reset } = await limiter.limit(identifier);

  // 3. Add rate limit headers (optional but nice for clients)
  const headers = {
    "X-RateLimit-Limit": "10",
    "X-RateLimit-Remaining": remaining.toString(),
    "X-RateLimit-Reset": reset.toString(),
  };

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

  // 4. Request allowed — do your thing
  return NextResponse.json(
    { message: "Hello!", remaining },
    { headers }
  );
}
5

Run your app

npm run dev
6

Test it

# Hit the endpoint multiple times
for i in {1..12}; do
  curl -X POST http://localhost:3000/api/protected \
    -H "x-user-id: test-user"
  echo ""
done
First 10 requests return 200. Requests 11+ return 429:
{ "error": "Too many requests. Please try again later." }
Wait 60 seconds and the limit resets.

What’s in the response?

limiter.limit() returns:
FieldTypeDescription
successbooleantrue if request is allowed, false if rate limited
remainingnumberRequests remaining in current window
resetnumberUnix timestamp (ms) when the window resets
limitnumberThe configured limit

Choosing an identifier

The identifier determines who gets rate limited. Common choices:
IdentifierUse caseExample
User IDAuthenticated usersreq.auth.userId
API keyPer-key limitsreq.headers.get("x-api-key")
IP addressAnonymous/public endpointsreq.headers.get("x-forwarded-for")
ComboExtra specificity${userId}:${endpoint}

Creating a reusable limiter

For cleaner code, create a utility:
lib/ratelimit.ts
import { Ratelimit } from "@unkey/ratelimit";

export const apiLimiter = new Ratelimit({
  rootKey: process.env.UNKEY_ROOT_KEY!,
  namespace: "api",
  limit: 100,
  duration: "1m",
});

export const authLimiter = new Ratelimit({
  rootKey: process.env.UNKEY_ROOT_KEY!,
  namespace: "auth",
  limit: 5,
  duration: "1m",
});
Then use in routes:
app/api/login/route.ts
import { authLimiter } from "@/lib/ratelimit";

export async function POST(req: Request) {
  const ip = req.headers.get("x-forwarded-for") ?? "unknown";
  const { success } = await authLimiter.limit(ip);
  
  if (!success) {
    return Response.json({ error: "Too many login attempts" }, { status: 429 });
  }
  
  // Handle login...
}

Next steps

Troubleshooting

  • Check that UNKEY_ROOT_KEY is set in .env.local
  • Verify your root key has ratelimit.*.limit permission
  • Make sure you’re using the same identifier each request
  • Restart the dev server after changing .env.local
Create multiple Ratelimit instances with different namespaces and limits. Each namespace tracks limits independently.
Last modified on February 16, 2026