SKILL.md
---
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
```bash
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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 `prefix` per endpoint or plan so limits do not collide.
- ✅ Return `Retry-After` and `X-RateLimit-*` headers with 429 responses.
- ✅ Prefer `slidingWindow` for most APIs; use `tokenBucket` when short bursts
are acceptable; use `fixedWindow` when the lowest Redis cost matters.
- ❌ Don't construct a new `Ratelimit` inside the request handler.
- ❌ Don't rely on `pending` completing on its own in edge runtimes.
- ❌ Don't rate limit by `x-forwarded-for` without 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.
- `MultiRegionRatelimit` trades 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 `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.
## Additional Resources
- [Upstash Ratelimit documentation](https://upstash.com/docs/redis/sdks/ratelimit-ts/overview)
- [@upstash/ratelimit on GitHub](https://github.com/upstash/ratelimit-js)
- [Algorithms guide](https://upstash.com/docs/redis/sdks/ratelimit-ts/algorithms)