rate-limit · diff

git:20260720.e99f6c4 to git:20260807.fac9f85

18 added, 209 removed. Audit A to A.

---
name: rate-limit
- description: Implement Upstash Redis rate limits with user or IP keys, 429 headers, and safe fallbacks. Use when protecting Next.js routes from abuse or cost overruns.
+ description: Implement or audit Upstash Redis rate limits with trusted user, tenant, API-key, or IP identifiers, 429 headers, and outage behavior. Use when protecting Next.js routes from abuse or cost.
license: MIT
---
- Wire rate limiting into your Next.js app using Upstash Redis sliding window. Reads the project first, applies IP-based limiting by default, user-based when auth is available.
-
- ## Three things that silently break rate limiting
-
- 1. **Wrong Redis key scope.** Using just the IP address as the key means all routes share one counter. A user who hits `/api/generate` 50 times uses up the budget for `/api/auth/login` too. Always namespace keys: `rate:generate:${ip}` not `rate:${ip}`.
- 2. **No fallback when Redis is down.** If Upstash times out and you throw, the entire route fails. Rate limiting is best-effort — fail open (allow the request) when the store is unreachable, and log the failure.
- 3. **Not returning `Retry-After`.** Clients that hit a 429 without a `Retry-After` header will retry immediately, making the problem worse. Always include it.
-
- ## Phase 1: Detect the Project
-
- ```bash
- cat package.json | grep -E "next|@upstash|upstash"
- ```
-
- - **Next.js present?** → proceed
- - **`@upstash/ratelimit` already installed?** → skip Phase 2, go to Phase 3
- - **Auth provider?** (Clerk, NextAuth, Supabase Auth) → determines whether user-based limiting is available
-
- ## Phase 2: Install and Configure
-
- ```bash
- npm install @upstash/ratelimit @upstash/redis
- ```
-
- Add to `.env.example`:
- ```
- UPSTASH_REDIS_REST_URL=
- UPSTASH_REDIS_REST_TOKEN=
- ```
-
- Tell the user: "Get these from console.upstash.com → your Redis database → REST API."
-
- Create the rate limiter config — one place, all limiters defined here:
-
- ```typescript
- // lib/rate-limit.ts
- import { Ratelimit } from '@upstash/ratelimit';
- import { Redis } from '@upstash/redis';
-
- const redis = Redis.fromEnv();
-
- export const rateLimiters = {
- // AI endpoints — expensive, tight limits
- ai: new Ratelimit({
- redis,
- limiter: Ratelimit.slidingWindow(10, '1 m'), // 10 req/min
- prefix: 'rate:ai',
- analytics: true,
- }),
- // Auth endpoints — loose limits, brute-force protection
- auth: new Ratelimit({
- redis,
- limiter: Ratelimit.slidingWindow(20, '1 m'), // 20 req/min
- prefix: 'rate:auth',
- analytics: true,
- }),
- // General API — default for everything else
- api: new Ratelimit({
- redis,
- limiter: Ratelimit.slidingWindow(60, '1 m'), // 60 req/min
- prefix: 'rate:api',
- analytics: true,
- }),
- };
- ```
-
- ## Phase 3: The Rate Limit Helper
-
- One helper that route handlers call. Fail open on Redis errors:
-
- ```typescript
- // lib/rate-limit.ts (add below rateLimiters)
- import { headers } from 'next/headers';
-
- export async function checkRateLimit(
- limiter: Ratelimit,
- identifier: string
- ): Promise<{ success: boolean; limit: number; remaining: number; reset: number }> {
- try {
- const result = await limiter.limit(identifier);
- return result;
- } catch (error) {
- // Redis unavailable — fail open, log it
- console.error('Rate limit check failed:', error);
- return { success: true, limit: 0, remaining: 0, reset: 0 };
- }
- }
-
- export function getIdentifier(req: Request, userId?: string): string {
- if (userId) return `user:${userId}`;
-
- // IP fallback — check forwarded headers for proxied deployments
- const forwarded = req.headers.get('x-forwarded-for');
- const ip = forwarded ? forwarded.split(',')[0].trim() : '127.0.0.1';
- return `ip:${ip}`;
- }
- ```
-
- ## Phase 4: Apply to Routes
-
- ### In a Next.js API route handler
-
- ```typescript
- // app/api/generate/route.ts
- import { rateLimiters, checkRateLimit, getIdentifier } from '@/lib/rate-limit';
-
- export async function POST(req: Request) {
- // Get identifier — prefer user ID if auth available
- const user = await getAuthUser(req); // your auth helper
- const identifier = getIdentifier(req, user?.id);
-
- const { success, limit, remaining, reset } = await checkRateLimit(
- rateLimiters.ai,
- identifier
- );
-
- if (!success) {
- return Response.json(
- { error: 'Too many requests. Try again in a moment.' },
- {
- status: 429,
- headers: {
- 'X-RateLimit-Limit': String(limit),
- 'X-RateLimit-Remaining': '0',
- 'Retry-After': String(Math.ceil((reset - Date.now()) / 1000)),
- },
- }
- );
- }
-
- // Add rate limit headers to successful responses too
- const response = await handleRequest(req);
- response.headers.set('X-RateLimit-Limit', String(limit));
- response.headers.set('X-RateLimit-Remaining', String(remaining));
- return response;
- }
- ```
-
- ### In Next.js middleware (blanket protection for a path prefix)
-
- ```typescript
- // middleware.ts
- import { NextResponse } from 'next/server';
- import type { NextRequest } from 'next/server';
- import { Ratelimit } from '@upstash/ratelimit';
- import { Redis } from '@upstash/redis';
-
- const ratelimit = new Ratelimit({
- redis: Redis.fromEnv(),
- limiter: Ratelimit.slidingWindow(30, '1 m'),
- prefix: 'rate:middleware',
- });
-
- export async function middleware(request: NextRequest) {
- // Only apply to API routes
- if (!request.nextUrl.pathname.startsWith('/api/')) {
- return NextResponse.next();
- }
-
- const ip = request.headers.get('x-forwarded-for')?.split(',')[0].trim() ?? '127.0.0.1';
-
- try {
- const { success, reset } = await ratelimit.limit(ip);
- if (!success) {
- return new NextResponse(JSON.stringify({ error: 'Too many requests' }), {
- status: 429,
- headers: {
- 'Content-Type': 'application/json',
- 'Retry-After': String(Math.ceil((reset - Date.now()) / 1000)),
- },
- });
- }
- } catch {
- // fail open
- }
-
- return NextResponse.next();
- }
-
- export const config = {
- matcher: '/api/:path*',
- };
- ```
-
- ## Phase 5: Ask the User
+ # Rate limiting with Upstash
- After detecting the stack, confirm:
+ Rate limits are one layer of abuse and cost control. Design policy before wiring middleware.
- ```
- I'll wire rate limiting with Upstash Redis.
+ ## Workflow
- Per-route limits (adjust if needed):
- - AI/generation endpoints: 10 req/min per user
- - Auth endpoints: 20 req/min per IP
- - General API: 60 req/min per user/IP
+ 1. Detect Next.js/runtime/version, deployed proxy/CDN, auth/API-key model, installed Upstash versions, protected routes, operation cost, existing quotas, and whether middleware/proxy already touches the request.
+ 2. Define a policy table per route class: identifier, algorithm/window/burst, cost weight, user tier, response behavior, and store-outage behavior. Ask only when the threat/cost tradeoff cannot be inferred.
+ 3. Prefer authenticated user, API key, or tenant+user identifiers. Use IP only for anonymous traffic and only from a deployment-provided trusted source. Never trust arbitrary `x-forwarded-for` from the public internet; configure trusted proxy depth/platform headers. Do not collapse unknown clients to `127.0.0.1`.
+ 4. Namespace by environment and route/policy. Do not apply blanket middleware and a per-route limiter to the same request unless deliberate layered limits use different keys.
+ 5. Configure explicit Upstash timeout and logging/metrics. Choose outage behavior by endpoint:
+ - low-risk availability paths may fail open with degraded telemetry;
+ - authentication, high-cost generation, or abuse-sensitive paths may use a local emergency limit, queue, credit reservation, or fail closed with a clear temporary response.
+ 6. Return `429` with a non-negative `Retry-After` and consistent rate-limit headers. Add headers to successful responses where useful. Do not emit misleading zero limits during fail-open degradation.
+ 7. For weighted work, consume tokens based on server-calculated batch/operation cost. Combine rate limits with body-size limits, concurrency controls, idempotency, budgets/credits, and provider quotas where relevant.
+ 8. Preserve async work such as analytics synchronization using the runtime’s `waitUntil` mechanism when the SDK exposes a pending promise.
- Which endpoints are highest priority to protect?
- And do you want middleware-level protection (all /api/* routes)
- or per-route (only specific handlers)?
- ```
+ ## Verification
- Apply to the routes they name. For AI endpoints (anything calling an LLM), always use the tightest limiter — cost overruns happen fast.
+ Use deterministic tests with an isolated key prefix and controllable clock/window where possible. Test allowed/blocked/reset behavior, route isolation, authenticated and anonymous keys, spoofed forwarding headers, paid/free policies, concurrency, weighted requests, store timeout/outage behavior, and duplicate middleware avoidance. Run repository lint/type/test/build and inspect Upstash analytics only when enabled.
- ## Verify
+ ## Output
- ```
- [ ] UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN in .env.example
- [ ] Rate limiter keys are namespaced by route prefix (rate:ai:, rate:auth:, etc.)
- [ ] 429 responses include Retry-After header
- [ ] Redis errors fail open (allow request, log error) — not throw
- [ ] AI/LLM endpoints use tightest limits
- [ ] Identifier uses user ID when available, IP as fallback
- [ ] Middleware or per-route applied based on user's preference
- [ ] Test: hit endpoint 11 times in 1 minute — 11th should 429
- ```
+ Report the policy table, identifier trust model, files/env names changed, outage behavior, response headers, layered controls, automated test evidence, and remaining Upstash/deployment configuration.