# Next.js

> Step-by-step Next.js rate limiting tutorial with @unkey/ratelimit. Throttle API routes and return 429 responses without provisioning Redis.

## 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

- [Unkey account](https://app.unkey.com/auth/sign-up) (free)
- [Root key](https://app.unkey.com/settings/root-keys) with `ratelimit.*.limit` permission
- Node.js 18+

<Steps titleSize="h3">

<Step title="Create a Next.js app">

Skip if you have an existing project.

<CodeGroup>

```bash npm
npx create-next-app@latest my-app
cd my-app
```

```bash pnpm
pnpm create next-app@latest my-app
cd my-app
```

```bash bun
bunx create-next-app my-app
cd my-app
```

</CodeGroup>

</Step>

<Step title="Install the SDK">

<CodeGroup>

```bash npm
npm install @unkey/ratelimit
```

```bash pnpm
pnpm add @unkey/ratelimit
```

```bash bun
bun add @unkey/ratelimit
```

</CodeGroup>

</Step>

<Step title="Add your root key">

Create or update `.env.local`:

```bash .env.local
UNKEY_ROOT_KEY="unkey_..."
```

<Warning>Never commit your root key. Add `.env.local` to `.gitignore`.</Warning>

</Step>

<Step title="Create a rate-limited route">

```ts 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 });
}
```

</Step>

<Step title="Run your app">

<CodeGroup>

```bash npm
npm run dev
```

```bash pnpm
pnpm dev
```

```bash bun
bun dev
```

</CodeGroup>

</Step>

<Step title="Test it">

```bash
# 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`:

```json
{ "error": "Too many requests. Please try again later." }
```

Wait 60 seconds and the limit resets.

</Step>

</Steps>

## What's in the response?

`limiter.limit()` returns:

| Field       | Type      | Description                                           |
| ----------- | --------- | ----------------------------------------------------- |
| `success`   | `boolean` | `true` if request is allowed, `false` if rate limited |
| `remaining` | `number`  | Requests remaining in current window                  |
| `reset`     | `number`  | Unix timestamp (ms) when the window resets            |
| `limit`     | `number`  | The configured limit                                  |

## Choosing an identifier

The identifier determines _who_ gets rate limited. Common choices:

| Identifier | Use case                   | Example                              |
| ---------- | -------------------------- | ------------------------------------ |
| User ID    | Authenticated users        | `req.auth.userId`                    |
| API key    | Per-key limits             | `req.headers.get("x-api-key")`       |
| IP address | Anonymous/public endpoints | `req.headers.get("x-forwarded-for")` |
| Combo      | Extra specificity          | `${userId}:${endpoint}`              |

## Creating a reusable limiter

For cleaner code, create a utility:

```ts 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:

```ts 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

<CardGroup cols={2}>
  <Card title="How it works" icon="bolt" href="/platform/ratelimiting/how-it-works">
    Understand the rate limiting architecture
  </Card>
  <Card
    title="Per-user overrides"
    icon="sliders"
    href="/platform/ratelimiting/overrides"
  >
    Give specific users higher limits
  </Card>
  <Card
    title="SDK Reference"
    icon="book"
    href="/libraries/ts/ratelimit/ratelimit"
  >
    All configuration options
  </Card>
  <Card title="Add API key auth" icon="key" href="/quickstart/apis/nextjs">
    Combine rate limiting with authentication
  </Card>
</CardGroup>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Rate limit not working?">
    - 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`
  </Accordion>

<Accordion title="Getting network errors?">
  - Unkey's SDK retries failed requests automatically - If errors persist, check
  [status.unkey.com](https://status.unkey.com) - Check
  [status.unkey.com](https://status.unkey.com) if issues persist
</Accordion>

  <Accordion title="Want different limits per route?">
    Create multiple `Ratelimit` instances with different namespaces and limits.
    Each namespace tracks limits independently.
  </Accordion>
</AccordionGroup>
