Live demo of the Whirlwind template, as shippedStripe test mode: card 4242 4242 4242 4242, any future date, any CVCGet Whirlwind →

Rate Limiting

rateLimitMiddleware with Upstash in production, plus credits as the spend limit.

What ships

lib/rateLimit.js exports rateLimitMiddleware. With UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN set it uses Upstash Redis, so limits hold across serverless instances. Without them it falls back to an in-memory map, which is fine locally and useless in production, where every instance keeps its own count. npm run deploy warns if the app URL is real and Upstash is unset.

Use it in a route

Key on the user ID for signed-in routes so one user cannot exhaust a shared IP, and keep limits per route.
import { rateLimitMiddleware } from "@/lib/rateLimit";

const RATE_LIMIT = { limit: 20, windowMs: 60 * 1000 };

export async function POST(req) {
  const auth = await getAuthenticatedUser(req);
  if (auth.error) return NextResponse.json({ error: auth.error }, { status: auth.status });

  const limited = await rateLimitMiddleware(req, { ...RATE_LIMIT, userId: auth.user.id });
  if (limited) return limited;   // a ready 429 response

  // validate → consume_credit → call the model → persist → return
}

Credits are the second limit

Rate limiting stops bursts. Credits stop runaway spend: consume_credit is an atomic database function, so two parallel requests cannot both succeed on the last credit.