From fa3345aa897d2087de47df15d8ad3a3a17733d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Y=C4=B1lmaz?= Date: Wed, 1 Jul 2026 14:24:59 +0000 Subject: [PATCH] example: Tiered Rate Limiting with Upstash Redis --- .../.env.example | 2 + .../nextjs-tiered-ratelimit-redis/README.md | 64 +++++++++ .../app/api/data/route.ts | 43 ++++++ .../app/layout.tsx | 16 +++ .../app/page.tsx | 126 ++++++++++++++++++ .../lib/ratelimit.ts | 22 +++ .../package.json | 23 ++++ .../tsconfig.json | 21 +++ 8 files changed, 317 insertions(+) create mode 100644 examples/nextjs-tiered-ratelimit-redis/.env.example create mode 100644 examples/nextjs-tiered-ratelimit-redis/README.md create mode 100644 examples/nextjs-tiered-ratelimit-redis/app/api/data/route.ts create mode 100644 examples/nextjs-tiered-ratelimit-redis/app/layout.tsx create mode 100644 examples/nextjs-tiered-ratelimit-redis/app/page.tsx create mode 100644 examples/nextjs-tiered-ratelimit-redis/lib/ratelimit.ts create mode 100644 examples/nextjs-tiered-ratelimit-redis/package.json create mode 100644 examples/nextjs-tiered-ratelimit-redis/tsconfig.json diff --git a/examples/nextjs-tiered-ratelimit-redis/.env.example b/examples/nextjs-tiered-ratelimit-redis/.env.example new file mode 100644 index 0000000..da8c4e8 --- /dev/null +++ b/examples/nextjs-tiered-ratelimit-redis/.env.example @@ -0,0 +1,2 @@ +UPSTASH_REDIS_REST_URL="" +UPSTASH_REDIS_REST_TOKEN="" diff --git a/examples/nextjs-tiered-ratelimit-redis/README.md b/examples/nextjs-tiered-ratelimit-redis/README.md new file mode 100644 index 0000000..22d0bb3 --- /dev/null +++ b/examples/nextjs-tiered-ratelimit-redis/README.md @@ -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=&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. diff --git a/examples/nextjs-tiered-ratelimit-redis/app/api/data/route.ts b/examples/nextjs-tiered-ratelimit-redis/app/api/data/route.ts new file mode 100644 index 0000000..3a69b84 --- /dev/null +++ b/examples/nextjs-tiered-ratelimit-redis/app/api/data/route.ts @@ -0,0 +1,43 @@ +import { NextRequest } from "next/server"; +import { limiters, Plan } from "@/lib/ratelimit"; + +const PLAN_LIMITS: Record = { + 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 } + ); +} diff --git a/examples/nextjs-tiered-ratelimit-redis/app/layout.tsx b/examples/nextjs-tiered-ratelimit-redis/app/layout.tsx new file mode 100644 index 0000000..9154496 --- /dev/null +++ b/examples/nextjs-tiered-ratelimit-redis/app/layout.tsx @@ -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 ( + + + {children} + + + ); +} diff --git a/examples/nextjs-tiered-ratelimit-redis/app/page.tsx b/examples/nextjs-tiered-ratelimit-redis/app/page.tsx new file mode 100644 index 0000000..88c58e6 --- /dev/null +++ b/examples/nextjs-tiered-ratelimit-redis/app/page.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { useState } from "react"; + +type Plan = "free" | "pro" | "enterprise"; + +const PLAN_CONFIG: Record = { + 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("free"); + const [userId, setUserId] = useState("user-123"); + const [results, setResults] = useState([]); + 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 ( +
+

+ Tiered Rate Limiting with Upstash Redis +

+

+ Test free (10/min), pro (100/min), and enterprise (1000/min) rate limits — each user gets their own sliding window bucket. +

+ +
+ + + +
+ + + + {results.length > 0 && ( +
+

Results (latest first)

+
+ {results.map((r, i) => ( +
+ + {r.status === 200 ? "200 OK" : "429 Too Many Requests"} + + + {r.remaining} / {r.limit} remaining + +
+ ))} +
+
+ )} +
+ ); +} diff --git a/examples/nextjs-tiered-ratelimit-redis/lib/ratelimit.ts b/examples/nextjs-tiered-ratelimit-redis/lib/ratelimit.ts new file mode 100644 index 0000000..1c7b60c --- /dev/null +++ b/examples/nextjs-tiered-ratelimit-redis/lib/ratelimit.ts @@ -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; diff --git a/examples/nextjs-tiered-ratelimit-redis/package.json b/examples/nextjs-tiered-ratelimit-redis/package.json new file mode 100644 index 0000000..8d02034 --- /dev/null +++ b/examples/nextjs-tiered-ratelimit-redis/package.json @@ -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" + } +} diff --git a/examples/nextjs-tiered-ratelimit-redis/tsconfig.json b/examples/nextjs-tiered-ratelimit-redis/tsconfig.json new file mode 100644 index 0000000..6420eed --- /dev/null +++ b/examples/nextjs-tiered-ratelimit-redis/tsconfig.json @@ -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"] +}