Imported from sickn33/agentic-awesome-skills (
skills/upstash-ratelimit/SKILL.md). Install upstream withnpx skills add sickn33/agentic-awesome-skills --skill upstash-ratelimit. Copyright stays with the author.
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 seconds
prefix: "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) {
const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
const { success, limit, remaining, reset } = await ratelimit.limit(ip);
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.ceil((reset - Date.now()) / 1000)),
},
});
}
return NextResponse.next();
}
export const config = { matcher: "/api/:path*" };
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; usefixedWindowwhen 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
- 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
undefinedor a fresh random value. - Problem: Analytics are empty on Vercel Edge or Cloudflare Workers.
Solution: Pass
pendingtowaitUntil(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.