| title | Algorithms |
|---|---|
| description | How fixed window, sliding window, token bucket, and cached fixed window algorithms work internally. |
Algorithms define how tokens are counted and when requests are allowed. In this library, algorithms are factory functions that return an object with limit, getRemaining, and resetTokens methods. The factories live in src/single.ts (single region) and src/multi.ts (multi region), and their Redis logic lives in src/lua-scripts/.
Each algorithm receives a Context with a Redis client, a key prefix, and optional cache. The algorithm then calls safeEval from src/hash.ts, which uses EVALSHA with a precomputed hash and falls back to EVAL if the script isn’t loaded.
flowchart TD
A[limit(identifier)] --> B{Algorithm}
B -->|fixedWindow| C[INCRBY + PEXPIRE]
B -->|slidingWindow| D[GET current + GET previous]
B -->|tokenBucket| E[HMGET refilledAt/tokens]
C --> F[Compare against limit]
D --> F
E --> F
F --> G[Return success/remaining/reset]
Fixed window is implemented in src/single.ts with SCRIPTS.singleRegion.fixedWindow.* in src/lua-scripts/single.ts. It increments a counter for the current window and rejects when the counter exceeds the limit. The Lua script sets the key’s expiration the first time it is created so each bucket is self‑cleaning.
Basic usage
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.fixedWindow(100, "1 m")
});
const res = await ratelimit.limit("api_key_123");Advanced usage (dynamic limits)
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.fixedWindow(60, "1 m"),
dynamicLimits: true
});
await ratelimit.setDynamicLimit({ limit: 120 });
const res = await ratelimit.limit("user_42");Sliding window blends current and previous windows to reduce boundary bursts. The Lua script reads both buckets, weights the previous window by how far into the current window you are, and then calculates remaining tokens. See SCRIPTS.singleRegion.slidingWindow.* in src/lua-scripts/single.ts.
Basic usage
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "10 s")
});Edge case (refunds)
If you pass a negative rate, the algorithm treats it as a refund and skips cache blocking. This is handled in src/single.ts by checking incrementBy > 0 before consulting the cache.
const res = await ratelimit.limit("order_77", { rate: -1 });Token bucket in src/single.ts uses a Redis hash to store refilledAt and tokens. The Lua script refills tokens based on elapsed time, then decrements by the request rate. See tokenBucketLimitScript in src/lua-scripts/single.ts.
Basic usage
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.tokenBucket(5, "10 s", 20)
});Advanced usage (higher burst)
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.tokenBucket(2, "1 s", 10)
});cachedFixedWindow is a special case that requires an ephemeral cache. It checks the local cache first, increments it optimistically, and updates Redis in the background. This is implemented in src/single.ts and uses cachedFixedWindow* scripts in src/lua-scripts/single.ts.
Basic usage
const cache = new Map();
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.cachedFixedWindow(5, "5 s"),
ephemeralCache: cache
});Advanced usage (fail fast)
try {
const res = await ratelimit.limit("ip:10.0.0.1");
if (!res.success) return new Response("blocked", { status: 429 });
} catch (error) {
// cachedFixedWindow throws if no cache is provided
}cachedFixedWindow requires a cache (ephemeralCache). If you create the Ratelimit instance inside a request handler, the cache resets on every request and you lose the speed benefits. Create the instance outside your handler in serverless or edge functions.