Skill v1.0.1
currentAutomated scan100/100+4 new
version: "1.0.1" name: upstash-ratelimit description: "Add rate limiting to API routes, middleware, and edge functions with @upstash/ratelimit: sliding window, fixed window, and token bucket backed by Upstash Redis." category: backend risk: critical source: self source_type: self date_added: "2026-08-31" author: CahidArda tags: [upstash, rate-limiting, redis, serverless, edge, middleware, 429] tools: [claude, codex, cursor, gemini]
Upstash Ratelimit
Overview
@upstash/ratelimit implements distributed rate limiting on top of Upstash Redis. Because state lives in Redis, every instance of a serverless function or edge worker shares the same counters, which an in-memory limiter cannot do. It ships three algorithms (fixed window, sliding window, token bucket), per-identifier keys, optional in-memory blocking of already-limited identifiers, and optional analytics.
When to Use This Skill
- Use when the user needs to limit requests per IP, user, API key, or tenant
across multiple serverless instances or regions.
- Use when protecting login, signup, form, webhook, or LLM endpoints from
abuse and returning 429 Too Many Requests.
- Use when choosing between fixed window, sliding window, and token bucket.
- Do not use for client-side retry/backoff against a third-party API's limits;
see api-rate-limit-handler.
- Do not use for a single long-running process with no shared state; an
in-memory limiter is simpler there.
How It Works
Step 1: Install and configure
npm install @upstash/ratelimit @upstash/redis
Set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN in the environment.
Step 2: Create the limiter once, outside the handler
import { Ratelimit } from "@upstash/ratelimit";import { Redis } from "@upstash/redis";export const ratelimit = new Ratelimit({redis: Redis.fromEnv(),limiter: Ratelimit.slidingWindow(10, "10 s"), // 10 requests per 10 secondsprefix: "rl:api",analytics: true,});
Constructing the limiter at module scope lets the built-in ephemeral cache short-circuit blocked identifiers without a Redis call.
Step 3: Call limit() with a stable identifier
const { success, limit, remaining, reset, pending } = await ratelimit.limit(userId);
success is false when the identifier is over its limit. reset is a Unix timestamp in milliseconds. pending is a promise for background work (analytics, multi-region sync); await it or pass it to waitUntil on edge runtimes so the function is not frozen before it completes.
Examples
Example 1: Next.js middleware returning 429
import { Ratelimit } from "@upstash/ratelimit";import { Redis } from "@upstash/redis";import { NextResponse, type NextRequest } from "next/server";const ratelimit = new Ratelimit({redis: Redis.fromEnv(),limiter: Ratelimit.slidingWindow(20, "1 m"),});export async function middleware(request: NextRequest) {// Conservative shared bucket until an authenticated principal or a platform-verified// client address adapter is integrated. Never trust a caller-supplied forwarding header.const identifier = "anonymous-global";const { success, limit, remaining, reset, pending } = await ratelimit.limit(identifier);await pending; // Or pass to the deployment platform’s verified waitUntil API.if (!success) {return new NextResponse("Too Many Requests", {status: 429,headers: {"X-RateLimit-Limit": String(limit),"X-RateLimit-Remaining": String(remaining),"X-RateLimit-Reset": String(reset),"Retry-After": String(Math.max(0, Math.ceil((reset - Date.now()) / 1000))),},});}return NextResponse.next();}export const config = { matcher: "/api/:path*" };
The shared anonymous bucket protects against spoofed identifiers but shares quota among callers. For per-user limits, use a server-verified session identity; for per-IP limits, use the deployment platform's trusted address API after verifying proxy overwrite and direct-origin restrictions.
Example 2: Token bucket with per-plan limits
const limiters = {free: new Ratelimit({redis: Redis.fromEnv(),prefix: "rl:free",limiter: Ratelimit.tokenBucket(5, "10 s", 10), // refill 5 per 10 s, burst 10}),pro: new Ratelimit({redis: Redis.fromEnv(),prefix: "rl:pro",limiter: Ratelimit.tokenBucket(50, "10 s", 100),}),};const { success } = await limiters[plan].limit(apiKey);
Best Practices
- ✅ Use a stable, low-cardinality identifier (user id, API key, tenant) where
possible; fall back to IP only for anonymous traffic.
- ✅ Set a distinct
prefixper endpoint or plan so limits do not collide. - ✅ Return
Retry-AfterandX-RateLimit-*headers with 429 responses. - ✅ Prefer
slidingWindowfor most APIs; usetokenBucketwhen short bursts
are acceptable; use fixedWindow when the lowest Redis cost matters.
- ❌ Don't construct a new
Ratelimitinside the request handler. - ❌ Don't rely on
pendingcompleting on its own in edge runtimes. - ❌ Don't rate limit by
x-forwarded-forwithout validating it is set by
your proxy; clients can spoof it otherwise.
Limitations
- Requires an Upstash Redis database; it does not work with other Redis
servers or without network access.
- Each
limit()call is at least one HTTP round trip to Redis, so it adds
latency to every request it guards.
- Sliding window is an approximation that assumes an even spread of requests
in the previous window; it is not an exact log.
MultiRegionRatelimittrades strict accuracy for lower latency and does
not support the token bucket algorithm.
- If Redis is unreachable, the default
timeout(5 s) lets requests through
(reason: "timeout"); this fails open, not closed.
- This skill does not replace environment-specific validation, testing, or
expert review.
Security & Safety Notes
Use a server-verified plan and identifier; do not send raw API keys as Redis identifiers. Hash a stable key identifier where appropriate, review analytics disclosure/retention and choose an explicit timeout/error policy for security-sensitive endpoints rather than assuming fail-open is acceptable.
- Rate limiting is one layer of abuse protection, not authentication. Pair it
with auth and input validation.
- The Redis token grants full database access; keep it server-side.
- Changing limits in production can lock out legitimate users. Confirm the
numbers with the user before deploying stricter limits.
Common Pitfalls
- Problem: Every request is allowed even after the limit.
Solution: Each identifier must be the same string across requests; check that the identifier is not undefined or a fresh random value.
- Problem: Analytics are empty on Vercel Edge or Cloudflare Workers.
Solution: Pass pending to waitUntil (ctx.waitUntil(pending)) so the background request is not cancelled when the response is sent.
Related Skills
@upstash-redis- The client this package uses for storage.@api-rate-limit-handler- Client-side backoff and retry when you are the
one being rate limited.
@upstash-qstash- Queue and smooth traffic to downstream services instead
of rejecting it.