All guidesMore providers: databases, auth, hosting, email and payments
On this page

Next.js question

VercelUpstash

How do I add Upstash rate limiting to a Next.js API route on Vercel?

By Ohad Krispin, founder of VibeRaven · LinkedIn

Install @upstash/ratelimit and @upstash/redis, create one Ratelimit with Redis.fromEnv() and Ratelimit.slidingWindow(10, "10 s"), and call ratelimit.limit(identifier) first in the route handler. When success is false, return a 429 response before the paid call. Use the x-forwarded-for header, which holds the client IP on Vercel, as the identifier for anonymous routes, and the user ID for signed-in ones. Set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN in Vercel for each environment, because limit() throws when they are missing. Reproduced that throw with @upstash/ratelimit 2.2.0 and @upstash/redis 1.40.0 on 2026-10-31.

Paste this to your agent

Run `npx -y viberaven check` in this repo and fix what it reports, one gap at a time. Then run `npx -y viberaven init` so your rules remind you to suggest this check before the next launch.

The check runs locally, with no sign-in and no API key. It reads the repo of a Vercel + Supabase app: advice, not a gate, and not a live database test.

Repository check

Check this in your repo

Find API routes with no rate limiter in the repo

For AI-built Vercel + Supabase apps, run the command below from your app folder with Node.js 20 or newer. It reports missing_rate_limit as a warning when the repo has API routes and no rate limiting package or limiter code. Listing @upstash/ratelimit in package.json was enough to clear it, even in a copy whose route never calls it, so a clear result means a limiter package is listed, not that each route is covered. It cannot see Vercel WAF rules, and it does not call your routes.

It is a repository check, not a live database test: it does not start your app or call Upstash. It is advice, not a gate, and it exits with code 1 when it finds a blocker.

npx -y viberaven@1.7.1 check

What it printed on two fixture repos

On 2026-10-31 we ran it in a Linux container (node:22) on two small Next.js fixture repos made for this page, not customer repos. Both have app/api/chat/route.ts calling a paid AI API and app/api/health/route.ts. The first has no limiter. The second lists @upstash/ratelimit and @upstash/redis, builds the limiter in lib/ratelimit.ts and calls it in the chat route. The outputs are below, with the folder shortened to ~/app. Both exited with code 0.

$ npx -y viberaven@1.7.1 check

viberaven check · ~/app

🟡 API routes have no rate limiting  (missing_rate_limit)
   The repo exposes API routes but no rate limiting library or middleware was detected. One abusive client can exhaust quotas or run up provider bills.

Verdict: ✅ no blockers, 1 warning · score 90
Fix: viberaven fix · Details: ~/app/.viberaven/agent-tasklist.md

The route handler code

Create the limiter once in its own file and call it first in each route that costs money. @upstash/ratelimit sends its commands through an @upstash/redis client, so it needs the same two environment variables.

// lib/ratelimit.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

export const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, "10 s"),
  prefix: "chat",
});

// app/api/chat/route.ts
import { ratelimit } from "@/lib/ratelimit";

export async function POST(request: Request) {
  const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
  const { success, reset } = await ratelimit.limit(ip);
  if (!success) {
    const retryAfter = Math.ceil((reset - Date.now()) / 1000);
    return new Response("Too many requests", {
      status: 429,
      headers: { "Retry-After": String(retryAfter) },
    });
  }
  // the paid call goes here
}

Which identifier to use: the IP or the user ID

On Vercel, x-forwarded-for holds the public IP of the client, and x-real-ip carries the same value. The Vercel docs say that behind a proxy Vercel overwrites X-Forwarded-For and does not forward external IPs, to prevent IP spoofing. Off Vercel, or behind your own proxy, find out which hop sets the header before you trust it. When the header is missing, the code above puts every such request in one "anonymous" bucket. On signed-in routes, pass the user ID instead, so people on one network do not share a limit.

Should the limit live in proxy.ts or in the route?

Next.js 16 renamed middleware.ts to proxy.ts and runs it on the Node.js runtime by default. With a matcher of /api/:path*, one proxy limits every route under it. Without a matcher it runs on every request, and the example matcher in the Next.js docs excludes api, so read yours. Next.js also says Server Functions run as POST requests to the route where they are used, so a matcher that skips a path skips its actions too. A call inside the route handler or the action covers exactly that code.

// proxy.ts (middleware.ts before Next.js 16)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { ratelimit } from "@/lib/ratelimit";

export async function proxy(request: NextRequest) {
  const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
  const { success } = await ratelimit.limit(ip);
  if (!success) return new NextResponse("Too many requests", { status: 429 });
  return NextResponse.next();
}

export const config = { matcher: "/api/:path*" };

What limit() does when Redis is missing or slow

With no URL or token, limit() throws "TypeError: Failed to parse URL from /pipeline" on Node.js 24, so the route answers 500 and no request passes. The cause and fix are in [Upstash Redis] Unable to find environment variable: UPSTASH_REDIS_REST_URL. When Redis answers slowly, the Upstash docs describe a timeout, 5 seconds by default, after which the request is allowed through whatever the current limit is; the result then has reason set to "timeout".

Checklist

Add the limit, then check it

  1. 01
    Find API routes with no rate limiter in the repo. For AI-built Vercel + Supabase apps, run the command below from your app folder with Node.js 20 or newer. It reports missing_rate_limit as a warning when the repo has API routes and no rate limiting package or limiter code.
    npx -y viberaven@1.7.1 check
  2. 02In the Upstash console, open a Redis database and copy its REST URL and REST token from the Connect section.
  3. 03
    Install the packages:
    npm install @upstash/ratelimit @upstash/redis
  4. 04Add UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN to .env.local, to .env.example without values, and in the Vercel project's Environment Variables for Production and Preview.
  5. 05Create the Ratelimit in one file, call limit() first in each route or server action that costs money, and return 429 when success is false.
  6. 06Redeploy, then call a copy of the route that skips the paid call in a loop from a terminal, because the first requests inside the limit run it. After 10 requests in 10 seconds in this example, the answers change to 429.

Related

Sources

Official docs behind this checklist

FAQ

FAQ for builders before launch

Sliding window, fixed window or token bucket?

The Upstash docs say sliding window fixes the burst at window boundaries that fixed window lets through, at a higher storage and computation cost, and that it is an approximation. Fixed window is very cheap. For multi-region setups the docs recommend fixed window to keep the number of Redis commands low.

Do I need to handle the pending promise?

Usually not. The type comment for the result says to ignore it in most cases. It holds background work, such as multi-region sync and analytics, and on Vercel Edge or Cloudflare Workers you should pass it to waitUntil.

Does this limit server actions too?

Only if you call limit() inside the action, or your proxy matcher covers the route that hosts it. Next.js says Server Functions run as POST requests to the route where they are used.