Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/nextjs-tiered-ratelimit-redis/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
UPSTASH_REDIS_REST_URL=""
UPSTASH_REDIS_REST_TOKEN=""
64 changes: 64 additions & 0 deletions examples/nextjs-tiered-ratelimit-redis/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
title: Tiered Rate Limiting with Upstash Redis
products: ["redis"]
stack: ["Next.js"]
use_cases: ["Ratelimiting"]
languages: ["ts"]
author: "upstash"
---

## What It Does

A minimal Next.js app that enforces three separate rate limit tiers — free (10 req/min), pro (100 req/min), and enterprise (1000 req/min) — against a single Upstash Redis database. Each user gets an isolated sliding-window bucket keyed by their user ID, so two free users never share quota.

The project contains:

- **`lib/ratelimit.ts`** — three `Ratelimit` instances (one per plan), created once and reused across requests.
- **`app/api/data/route.ts`** — a Next.js Route Handler that reads `userId` and `plan` from the query string, checks the right limiter, and returns `X-RateLimit-Limit` / `X-RateLimit-Remaining` headers on every response.
- **`app/page.tsx`** — a browser UI to fire requests, switch plans and user IDs, and watch the counters tick down in real time.

## Prerequisites

- Node.js 18+
- An [Upstash Redis](https://console.upstash.com/) database (free tier is fine)

## Setup

1. **Clone and install**

```bash
git clone https://github.com/upstash/examples
cd examples/nextjs-tiered-ratelimit-redis
npm install
```

2. **Set environment variables**

Copy `.env.example` to `.env.local` and fill in your Upstash Redis credentials from the Upstash console:

```bash
cp .env.example .env.local
```

```
UPSTASH_REDIS_REST_URL="https://your-db.upstash.io"
UPSTASH_REDIS_REST_TOKEN="your-token"
```

3. **Run**

```bash
npm run dev
```

Open [http://localhost:3000](http://localhost:3000), choose a plan and user ID, and click **Send Request** to see rate limiting in action.

## How It Works

```
GET /api/data?userId=<id>&plan=free|pro|enterprise
```

The handler picks the matching `Ratelimit` instance and calls `.limit(userId)`. If the sliding window is full it returns `429`; otherwise `200` with the remaining quota in the response headers.

The free-tier limiter has `analytics: true`, which sends per-identifier telemetry to the Upstash Rate Limit dashboard — useful for spotting users who consistently hit their ceiling and are ready to upgrade.
43 changes: 43 additions & 0 deletions examples/nextjs-tiered-ratelimit-redis/app/api/data/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { NextRequest } from "next/server";
import { limiters, Plan } from "@/lib/ratelimit";

const PLAN_LIMITS: Record<Plan, number> = {
free: 10,
pro: 100,
enterprise: 1000,
};

export async function GET(req: NextRequest) {
const searchParams = req.nextUrl.searchParams;
const userId = searchParams.get("userId") ?? req.headers.get("x-forwarded-for") ?? "anonymous";
const plan = (searchParams.get("plan") ?? "free") as Plan;

if (!(plan in limiters)) {
return Response.json({ error: "Invalid plan." }, { status: 400 });
}

const { success, limit, remaining } = await limiters[plan].limit(userId);

const headers = {
"X-RateLimit-Limit": String(limit),
"X-RateLimit-Remaining": String(remaining),
};

if (!success) {
return Response.json(
{ error: "Rate limit exceeded. Upgrade for higher limits." },
{ status: 429, headers: { ...headers, "X-RateLimit-Remaining": "0" } }
);
}

return Response.json(
{
message: "Request successful.",
userId,
plan,
limit: PLAN_LIMITS[plan],
remaining,
},
{ status: 200, headers }
);
}
16 changes: 16 additions & 0 deletions examples/nextjs-tiered-ratelimit-redis/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "Tiered Rate Limiting with Upstash Redis",
description: "Demonstrate free, pro, and enterprise rate limits using @upstash/ratelimit.",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body style={{ fontFamily: "system-ui, sans-serif", maxWidth: 640, margin: "48px auto", padding: "0 16px" }}>
{children}
</body>
</html>
);
}
126 changes: 126 additions & 0 deletions examples/nextjs-tiered-ratelimit-redis/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"use client";

import { useState } from "react";

type Plan = "free" | "pro" | "enterprise";

const PLAN_CONFIG: Record<Plan, { limit: number; color: string }> = {
free: { limit: 10, color: "#6b7280" },
pro: { limit: 100, color: "#2563eb" },
enterprise: { limit: 1000, color: "#7c3aed" },
};

interface Result {
status: number;
limit: number;
remaining: number;
message?: string;
error?: string;
}

export default function Home() {
const [plan, setPlan] = useState<Plan>("free");
const [userId, setUserId] = useState("user-123");
const [results, setResults] = useState<Result[]>([]);
const [loading, setLoading] = useState(false);

async function sendRequest() {
setLoading(true);
try {
const res = await fetch(`/api/data?userId=${encodeURIComponent(userId)}&plan=${plan}`);
const data = await res.json();
const limit = Number(res.headers.get("X-RateLimit-Limit") ?? PLAN_CONFIG[plan].limit);
const remaining = Number(res.headers.get("X-RateLimit-Remaining") ?? 0);
setResults((prev) => [{ status: res.status, limit, remaining, ...data }, ...prev].slice(0, 20));
} finally {
setLoading(false);
}
}

const cfg = PLAN_CONFIG[plan];

return (
<main>
<h1 style={{ fontSize: "1.5rem", fontWeight: 700, marginBottom: 4 }}>
Tiered Rate Limiting with Upstash Redis
</h1>
<p style={{ color: "#6b7280", marginBottom: 32 }}>
Test free (10/min), pro (100/min), and enterprise (1000/min) rate limits — each user gets their own sliding window bucket.
</p>

<div style={{ display: "flex", flexDirection: "column", gap: 16, marginBottom: 24 }}>
<label>
<span style={{ display: "block", fontSize: "0.875rem", fontWeight: 600, marginBottom: 4 }}>User ID</span>
<input
value={userId}
onChange={(e) => setUserId(e.target.value)}
style={{ width: "100%", padding: "8px 12px", border: "1px solid #d1d5db", borderRadius: 6, fontSize: "0.875rem", boxSizing: "border-box" }}
/>
</label>

<label>
<span style={{ display: "block", fontSize: "0.875rem", fontWeight: 600, marginBottom: 4 }}>Plan</span>
<div style={{ display: "flex", gap: 8 }}>
{(["free", "pro", "enterprise"] as Plan[]).map((p) => (
<button
key={p}
onClick={() => setPlan(p)}
style={{
flex: 1, padding: "8px 0", borderRadius: 6, border: "2px solid",
borderColor: plan === p ? PLAN_CONFIG[p].color : "#d1d5db",
background: plan === p ? PLAN_CONFIG[p].color : "white",
color: plan === p ? "white" : "#374151",
fontWeight: 600, cursor: "pointer", textTransform: "capitalize",
}}
>
{p}
<span style={{ display: "block", fontSize: "0.75rem", fontWeight: 400 }}>
{PLAN_CONFIG[p].limit}/min
</span>
</button>
))}
</div>
</label>
</div>

<button
onClick={sendRequest}
disabled={loading}
style={{
width: "100%", padding: "10px 0", borderRadius: 6, border: "none",
background: cfg.color, color: "white", fontWeight: 700, fontSize: "1rem",
cursor: loading ? "not-allowed" : "pointer", opacity: loading ? 0.7 : 1,
marginBottom: 24,
}}
>
{loading ? "Sending…" : "Send Request"}
</button>

{results.length > 0 && (
<div>
<h2 style={{ fontSize: "1rem", fontWeight: 600, marginBottom: 12 }}>Results (latest first)</h2>
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{results.map((r, i) => (
<div
key={i}
style={{
padding: "10px 14px", borderRadius: 6, fontSize: "0.875rem",
background: r.status === 200 ? "#f0fdf4" : "#fef2f2",
border: `1px solid ${r.status === 200 ? "#bbf7d0" : "#fecaca"}`,
display: "flex", justifyContent: "space-between", alignItems: "center",
}}
>
<span style={{ fontWeight: 600, color: r.status === 200 ? "#15803d" : "#dc2626" }}>
{r.status === 200 ? "200 OK" : "429 Too Many Requests"}
</span>
<span style={{ color: "#6b7280" }}>
{r.remaining} / {r.limit} remaining
</span>
</div>
))}
</div>
</div>
)}
</main>
);
}
22 changes: 22 additions & 0 deletions examples/nextjs-tiered-ratelimit-redis/lib/ratelimit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const redis = Redis.fromEnv();

export const limiters = {
free: new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, "1 m"),
analytics: true,
}),
pro: new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(100, "1 m"),
}),
enterprise: new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(1000, "1 m"),
}),
};

export type Plan = keyof typeof limiters;
23 changes: 23 additions & 0 deletions examples/nextjs-tiered-ratelimit-redis/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "nextjs-tiered-ratelimit-redis",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@upstash/ratelimit": "^2.0.5",
"@upstash/redis": "^1.34.3",
"next": "^15.3.3",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.8.3"
}
}
21 changes: 21 additions & 0 deletions examples/nextjs-tiered-ratelimit-redis/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}