# Upstash Ratelimit

> Add rate limiting to API routes, middleware, and edge functions with @upstash/ratelimit: sliding window, fixed window, and token bucket backed by Upstash Redis.

- Skill: `francostino/upstash-ratelimit` (Agent Skill)
- Install (CLI): `npx skillmds@latest add francostino/upstash-ratelimit`
- Raw SKILL.md: https://api.skillmd.com/api/skills/francostino/upstash-ratelimit/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: FrancoStino (https://skillmd.com/u/francostino)
- Updated: 2026-09-08
- Page: https://skillmd.com/skills/francostino/upstash-ratelimit

---


# 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)

