Add rate limiting to API routes, middleware, and edge functions with @upstash/ratelimit: sliding window, fixed window, and token bucket backed by Upstash Redis.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add sickn33/agentic-awesome-skills --skill upstash-ratelimit --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Upstash Ratelimit?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/sickn33-upstash-ratelimit)More formats (shields.io, HTML) on the badges page.
---
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)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!