Skip to content
Back to skills

Upstash Ratelimit

ASecurity

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

  • 8 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
developmenttypescriptrustgobashnextjstestinggitapidatabasebackend

Works with

  • cursor
  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned November 7, 2026

npx -y skills add huytran1120/vietnam-agentic-skills --skill upstash-ratelimit --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Upstash Ratelimit?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Upstash Ratelimit
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/huytran1120-upstash-ratelimit/badge)](https://www.skillsdirectory.com/skills/huytran1120-upstash-ratelimit)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
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) {
  // 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

```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

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.

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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.