Upstash Ratelimit

← Back to skills

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

Category: General & Miscellaneous
Repo: antigravity-awesome-skills
Path: skills/upstash-ratelimit/SKILL.md
Updated: 9/7/2026, 7:23:33 AM

AI Summary

`@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. It is useful for general automation, multi-purpose workflows, cross-disciplinary tasks, and utility skills. Source: antigravity-awesome-skills (skills/upstash-ratelimit/SKILL.md).

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

Related skills