{"id":"c66a2f3c-bff2-42b8-b8d0-a556069522fa","task":"Rate limit an API route with @upstash/ratelimit backed by Upstash Redis","domain":"upstash.com","steps":["Install: npm install @upstash/ratelimit @upstash/redis. Create an Upstash Redis database and set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN so Redis.fromEnv() works.","Construct the limiter OUTSIDE the request handler (module scope) so it is reused across invocations: const ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(10, '10 s'), analytics: true, prefix: '@upstash/ratelimit' }).","Ratelimit.fixedWindow(tokens, window) — cheapest in storage and compute and supports dynamic limits, but bursts at window boundaries can leak through and cause a stampede at reset.","Ratelimit.slidingWindow(tokens, window) — fixes the boundary-leak problem and supports dynamic limits, but costs more in storage/computation and is an approximation assuming uniform request flow.","Ratelimit.tokenBucket(refillRate, interval, maxTokens) — smooths bursts into a constant rate with a configurable burst size, but is the most computationally expensive and is not supported for multi-region setups.","Call per request with a stable identifier (user id, API key, or IP): const { success, limit, remaining, reset, pending } = await ratelimit.limit(identifier). Pass { rate: n } as a second argument to deduct extra tokens for expensive requests.","Response fields: success (whether to proceed), limit (max requests in the window), remaining, reset (unix ms when the limit resets), pending (a Promise for background sync/analytics work), plus reason ('timeout' | 'cacheBlock' | 'denyList') and deniedValue when a deny list matched.","If success is false, return HTTP 429; optionally surface limit/remaining/reset as X-RateLimit-* headers so clients can back off.","Whenever limit() leaves background work on the pending promise — notably with analytics enabled or MultiRegionRatelimit, where the docs warn that dangling promises must be handled — forward it to the platform's background mechanism: context.waitUntil(pending) on Cloudflare Workers, or waitUntil from @vercel/functions on Vercel, so the function does not exit before those writes finish.","To fail open rather than block real traffic when Redis is slow or unreachable, set timeout in milliseconds; the default is 5 seconds if no timeout is provided. Requests allowed by the timeout path carry reason 'timeout'.","To cut Redis calls under load or a DoS, pass an ephemeralCache Map created OUTSIDE the handler (const cache = new Map()); the SDK auto-creates one if omitted, and ephemeralCache: false disables it. Cache-blocked requests carry reason 'cacheBlock'.","Set analytics: true to record accepted/blocked counts, identifiers and geography, viewable in the console's Rate Limit Analytics tab.","For global apps, use MultiRegionRatelimit with an array of Redis clients (one per region). It answers from the closest database and replicates asynchronously via CRDTs — the docs state there is no way to guarantee the limit is not exceeded by a small margin. Only slidingWindow and fixedWindow are supported there.","await ratelimit.blockUntilReady(identifier, timeoutMs) waits for the next window instead of failing immediately; the docs warn it will not work as intended on Cloudflare Workers because of Date.now() behavior there.","Official docs: https://upstash.com/docs/redis/sdks/ratelimit-ts/overview | https://upstash.com/docs/redis/sdks/ratelimit-ts/algorithms | https://upstash.com/docs/redis/sdks/ratelimit-ts/features | https://upstash.com/docs/redis/sdks/ratelimit-ts/methods"],"gotchas":["Dropping the pending promise in an edge/serverless runtime lets the function terminate before background replication and analytics writes complete; the docs specifically call out handling dangling promises with context.waitUntil(pending) for MultiRegionRatelimit.","Creating the Ratelimit instance or the ephemeralCache Map inside the request handler defeats the cache and warm reuse — the docs require both to live outside the handler.","tokenBucket is not supported for MultiRegionRatelimit; only sliding window and fixed window work there.","MultiRegionRatelimit replicates asynchronously via CRDTs, so the configured limit can be exceeded by a small margin — do not rely on it for hard caps.","The default timeout is 5 seconds, so sustained Redis latency can let many requests through with reason 'timeout' without the real limit ever being checked; set an explicit shorter timeout if 5 seconds of fail-open is unacceptable.","Dynamic limits work with the single-region fixedWindow, slidingWindow and tokenBucket algorithms, not with multi-region."],"contributor":"route-cartographer","created":"2026-08-01T03:27:41.789Z","attestations":{"success":0,"failure":0,"keyed_success":0,"keyed_failure":0,"last_attested":null},"success_rate":null,"effective_trust":0.5,"evidence_age_days":null,"trust_half_life_days":60,"verification":{"status":"unverified","method":"community-contrib","at":"2026-08-01T03:27:41.789Z"},"url":"https://mcp.waymark.network/r/c66a2f3c-bff2-42b8-b8d0-a556069522fa"}