api-database-upstash
Upstash serverless Redis -- REST-based client, auto-serialization, pipelines, rate limiting, QStash, edge compatibility, global replication
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-database-upstash/skills/api-database-upstash
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Upstash Patterns
Quick Guide: Upstash provides a REST/HTTP-based Redis client (
@upstash/redis) designed for serverless and edge runtimes where TCP connections are unavailable. Unlike ioredis/node-redis, every command is an HTTP request -- no persistent connections, no connection pools, no teardown. The client automatically serializes/deserializes JSON (objects stored viasetcome back as objects fromget), which is convenient but has gotchas with large numbers and cross-client compatibility. Useredis.pipeline()to batch commands into a single HTTP request,redis.multi()for atomic transactions, and@upstash/ratelimitfor pre-built rate limiting algorithms. For background jobs, use@upstash/qstashwhich pushes messages to your API via HTTP webhooks.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use Redis.fromEnv() for initialization in production code -- never hardcode UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN values)
(You MUST handle the pending promise from @upstash/ratelimit responses in edge runtimes -- use context.waitUntil(pending) on Vercel Edge/Cloudflare Workers or analytics data is lost)
(You MUST use redis.pipeline() when issuing 3+ independent commands in a single handler -- each command is a separate HTTP round-trip without pipelining)
(You MUST NOT use Upstash for Pub/Sub, blocking commands (BRPOP, BLPOP, XREAD BLOCK), or Lua scripting -- REST API does not support these; use ioredis with a TCP connection instead)
</critical_requirements>
Examples
- Core Patterns -- Client setup, commands, auto-serialization, pipeline, transactions
- Rate Limiting -- @upstash/ratelimit algorithms, middleware, analytics
- QStash -- Background jobs, scheduling, message publishing
Additional resources:
- reference.md -- Command cheat sheet, constructor options, environment variables, eviction policies
Auto-detection: Upstash, @upstash/redis, @upstash/ratelimit, @upstash/qstash, Redis.fromEnv, UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN, Ratelimit.slidingWindow, Ratelimit.fixedWindow, Ratelimit.tokenBucket, serverless Redis, edge Redis, REST Redis
When to use:
- Serverless functions (AWS Lambda, Vercel, Netlify) that cannot maintain TCP connections
- Edge runtimes (Cloudflare Workers, Vercel Edge, Fastly Compute) that only support HTTP
- Rate limiting API routes with pre-built algorithms (sliding window, fixed window, token bucket)
- Caching in serverless/edge where ioredis connection pooling is impractical
- Background job scheduling with QStash (push-based, no long-running consumers needed)
- Global read latency optimization via Upstash Global Database with read replicas
Key patterns covered:
@upstash/redisclient setup withRedis.fromEnv()and constructor options- Automatic JSON serialization/deserialization behavior and gotchas
- Pipeline batching (
redis.pipeline()) and atomic transactions (redis.multi()) @upstash/ratelimitalgorithms: sliding window, fixed window, token bucket@upstash/qstashfor serverless background jobs and scheduling- Global Database architecture (primary + read regions, eventual consistency)
- Edge runtime compatibility and
context.waitUntil()patterns
When NOT to use:
- Long-running servers with persistent connections (use ioredis -- lower latency per command via TCP)
- Pub/Sub, blocking commands, or Lua scripting (REST API does not support these)
- Write-heavy workloads on Global Database (writes always go to primary region)
- Latency-critical paths where per-command HTTP overhead (~5-15ms) is unacceptable (use ioredis with TCP for <1ms per command)
- Large payloads (>1 MB) -- REST API has payload size limits
<decision_framework>
Decision Framework
Upstash vs ioredis/node-redis
Which Redis client should I use?
|-- Running in edge runtime (Cloudflare Workers, Vercel Edge)?
| --> @upstash/redis (only option -- no TCP available)
|-- Running in serverless (Lambda, Vercel Serverless)?
| |-- Short-lived functions with no connection reuse?
| | --> @upstash/redis (no connection management overhead)
| |-- Long-lived functions with connection pooling?
| --> ioredis (lower per-command latency)
|-- Running on a persistent server (Docker, EC2, K8s)?
| --> ioredis (persistent TCP = <1ms latency vs ~5-15ms HTTP)
|-- Need Pub/Sub, blocking commands, or Lua scripts?
| --> ioredis (REST API cannot support these)
|-- Need to run in browser or WebAssembly?
--> @upstash/redis (HTTP works everywhere)
Which Rate Limiting Algorithm?
Which @upstash/ratelimit algorithm should I use?
|-- Need strict, evenly distributed limiting?
| --> slidingWindow -- smoothest, no burst-at-boundary issues
|-- Need simple, low-overhead limiting?
| --> fixedWindow -- cheapest computationally, allows boundary bursts
|-- Need to allow burst traffic up to a capacity?
| --> tokenBucket -- smooths bursts, allows initial spike up to maxTokens
|-- Need multi-region rate limiting?
--> fixedWindow (slidingWindow has high Redis command overhead in multi-region)
Pipeline vs Transaction vs Sequential
How should I batch these Redis commands?
|-- Commands are independent (no ordering dependency)?
| --> Pipeline (redis.pipeline()) -- non-atomic but single HTTP request
|-- Commands must execute atomically (all-or-nothing)?
| --> Transaction (redis.multi()) -- atomic, single HTTP request
|-- Only 1-2 commands?
--> Sequential is fine -- pipeline overhead not worth it
Global Database vs Regional
Should I use Upstash Global Database?
|-- Read-heavy workload with users worldwide?
| --> Global Database -- reads from nearest replica
|-- Write-heavy workload?
| --> Regional Database -- writes always go to primary, replication doubles write cost
|-- Need strong consistency?
| --> Regional Database -- Global is eventually consistent
|-- Latency-sensitive reads from multiple continents?
--> Global Database -- sub-1ms reads from nearest region
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Using
JSON.stringify()before passing objects toredis.set()-- auto-serialization already handles this, resulting in double-encoded strings like"{\"name\":\"Alice\"}"that break on read - Ignoring the
pendingpromise fromratelimit.limit()in edge runtimes -- analytics data and multi-region sync are lost silently; usecontext.waitUntil(pending) - Issuing 5+ sequential
await redis.get/set()calls without pipelining -- each is a separate HTTP request, adding 25-75ms of unnecessary latency - Attempting Pub/Sub (
redis.subscribe), blocking commands (BRPOP,BLPOP), or Lua scripting (eval) -- Upstash REST API does not support these; use ioredis with TCP
Medium Priority Issues:
- Missing TTL on cached keys -- same as any Redis: unbounded memory growth until eviction kicks in
- Using Global Database for write-heavy workloads -- writes always route to primary region and replication doubles command costs
- Not setting
automaticDeserialization: falsewhen interoperating with non-Upstash clients -- other clients store raw strings, Upstash will fail to parse them as JSON - Creating a new
Redisinstance per request instead of reusing a module-level singleton -- while connectionless, the client still benefits from HTTP keep-alive and warm connections
Common Mistakes:
- Expecting
redis.get()to return a string when an object was stored -- auto-deserialization returns the original object type, not a JSON string - Assuming pipeline execution is atomic -- pipelines batch for network efficiency but other clients can interleave; use
redis.multi()for atomicity - Using
Ratelimit.slidingWindowwithMultiRegionRatelimit-- sliding window has high Redis command overhead in multi-region setups; usefixedWindowinstead - Storing values larger than 1 MB -- REST API has payload size limits; store references and fetch large data from object storage
Gotchas & Edge Cases:
- Large numbers become strings: JavaScript cannot safely handle numbers >
2^53 - 1(Number.MAX_SAFE_INTEGER). Upstash returns these as strings even when the TypeScript type saysnumber. Always validate large numeric values. - Base64 encoding by default: The SDK requests base64-encoded responses to handle edge cases. If you see garbled output like
dmFsdWU=, the response encoding is interfering -- checkresponseEncodingoption. redis.get()returnsnullfor missing keys, notundefined: This matters for TypeScript narrowing -- checkresult !== null, not truthiness.- SET options use an object, not positional args: Upstash uses
redis.set("key", "value", { ex: 300 })notredis.set("key", "value", "EX", 300)-- the ioredis positional argument style does not work. - Global Database is eventually consistent: A write followed immediately by a read from a different region may return stale data. Design for eventual consistency or use regional database for strong consistency.
hgetallreturns an empty object{}for non-existent keys: CheckObject.keys(result).length === 0, notresult === null.blockUntilReady()does not work on Cloudflare Workers: Cloudflare'sDate.now()behaves differently; uselimit()with manual retry logic instead.- No WATCH command: Upstash REST API does not support
WATCHfor optimistic locking. Useredis.multi()for atomic operations or implement application-level optimistic concurrency. - Auto-pipelining is available: The SDK can automatically batch commands issued during the same event loop tick via
enableAutoPipelining: truein the constructor.
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use Redis.fromEnv() for initialization in production code -- never hardcode UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN values)
(You MUST handle the pending promise from @upstash/ratelimit responses in edge runtimes -- use context.waitUntil(pending) on Vercel Edge/Cloudflare Workers or analytics data is lost)
(You MUST use redis.pipeline() when issuing 3+ independent commands in a single handler -- each command is a separate HTTP round-trip without pipelining)
(You MUST NOT use Upstash for Pub/Sub, blocking commands (BRPOP, BLPOP, XREAD BLOCK), or Lua scripting -- REST API does not support these; use ioredis with a TCP connection instead)
Failure to follow these rules will cause credential leaks, silent data loss in edge runtimes, unnecessary latency from sequential HTTP requests, and runtime errors from unsupported commands.
</critical_reminders>
Files (skills)
-
examples
-
core.md 9.8 KB
# Upstash -- Core Examples > Client setup, commands, auto-serialization, pipeline, and transaction patterns. Reference from [SKILL.md](../SKILL.md). **Related examples:** - [rate-limiting.md](rate-limiting.md) -- @upstash/ratelimit algorithms, middleware, analytics - [qstash.md](qstash.md) -- Background jobs, scheduling, message publishing --- ## Client Initialization ### Redis.fromEnv() (Preferred) ```typescript import { Redis } from "@upstash/redis"; // Reads UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN from environment const redis = Redis.fromEnv(); export { redis }; ``` **Why good:** Zero-config, environment variables injected by platform (Vercel, Fly.io, Heroku), no secrets in code, works across all deployment environments ### Explicit Constructor ```typescript import { Redis } from "@upstash/redis"; const REQUEST_TIMEOUT_MS = 5000; const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL!, token: process.env.UPSTASH_REDIS_REST_TOKEN!, automaticDeserialization: true, signal: () => AbortSignal.timeout(REQUEST_TIMEOUT_MS), }); export { redis }; ``` **When to use:** When you need to configure timeout, disable auto-deserialization, or use a non-standard env var name. --- ## Disabling Auto-Deserialization When interoperating with non-Upstash clients (ioredis, redis-cli) that store raw strings: ```typescript import { Redis } from "@upstash/redis"; const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL!, token: process.env.UPSTASH_REDIS_REST_TOKEN!, automaticDeserialization: false, }); // Now redis.get() returns raw strings -- you must JSON.parse manually const raw = await redis.get<string>("key"); if (raw !== null) { const parsed = JSON.parse(raw); } export { redis }; ``` **When to use:** When another service writes raw strings to the same Redis instance and Upstash's auto-deserialization would fail to parse them. --- ## Auto-Serialization Behavior ### Objects Round-Trip Automatically ```typescript interface UserProfile { name: string; email: string; preferences: { theme: string; language: string }; } const CACHE_TTL_SECONDS = 3600; // Store an object -- auto-serialized with JSON.stringify await redis.set<UserProfile>( "user:123", { name: "Alice", email: "alice@example.com", preferences: { theme: "dark", language: "en" }, }, { ex: CACHE_TTL_SECONDS }, ); // Retrieve -- auto-deserialized with JSON.parse const user = await redis.get<UserProfile>("user:123"); // user is UserProfile | null -- NOT a string if (user !== null) { // user.preferences.theme => "dark" -- direct property access } ``` **Why good:** No manual `JSON.stringify`/`JSON.parse`, TypeScript generic provides type safety, nested objects preserved ### The Double-Serialization Trap ```typescript // BAD -- double-encoding await redis.set("user:123", JSON.stringify({ name: "Alice" })); // Stored as: "\"{ \\\"name\\\": \\\"Alice\\\" }\"" const result = await redis.get("user:123"); // result is the STRING '{"name":"Alice"}' -- NOT an object // Because auto-serialization already called JSON.stringify on your string ``` **Why bad:** `JSON.stringify` is called automatically. Calling it manually means Upstash serializes the already-serialized string, producing double-encoded JSON. ### Numbers and Primitive Values ```typescript // Numbers are preserved as numbers await redis.set("counter", 42); const count = await redis.get<number>("counter"); // count is 42 (number), not "42" (string) // BUT: Large numbers become strings const LARGE_ID = "101600000000150081467"; await redis.set("big-id", LARGE_ID); const bigId = await redis.get<string>("big-id"); // bigId is "101600000000150081467" (string) because > Number.MAX_SAFE_INTEGER ``` **Gotcha:** Numbers larger than `2^53 - 1` (9007199254740991) are returned as strings regardless of TypeScript type annotation. Always validate large numeric values. --- ## Pipeline Batching ### Basic Pipeline ```typescript import { Redis } from "@upstash/redis"; const redis = Redis.fromEnv(); const USER_TTL_SECONDS = 3600; async function cacheUserData(userId: string, name: string, email: string) { const pipe = redis.pipeline(); pipe.set(`user:${userId}:name`, name, { ex: USER_TTL_SECONDS }); pipe.set(`user:${userId}:email`, email, { ex: USER_TTL_SECONDS }); pipe.incr("stats:total-users"); pipe.sadd("users:active", userId); // Single HTTP request for all 4 commands const [nameResult, emailResult, totalUsers, addedCount] = await pipe.exec<["OK", "OK", number, number]>(); return { totalUsers, isNewActiveUser: addedCount === 1 }; } export { cacheUserData }; ``` **Why good:** 4 commands in 1 HTTP request, typed results with generics, named TTL constant ### Chained Pipeline Syntax ```typescript const results = await redis .pipeline() .set("key1", "value1") .set("key2", "value2") .get("key1") .exec<["OK", "OK", string]>(); ``` **When to use:** Quick inline batching when you don't need to conditionally add commands. --- ## Transactions (Multi/Exec) ### Atomic Counter Update ```typescript import { Redis } from "@upstash/redis"; const redis = Redis.fromEnv(); async function atomicOrderUpdate(orderId: string) { const tx = redis.multi(); tx.incr("orders:count"); tx.set(`order:${orderId}:status`, "confirmed"); tx.set(`order:${orderId}:updated-at`, Date.now()); // All 3 commands execute atomically const [count, status1, status2] = await tx.exec<[number, "OK", "OK"]>(); return { orderNumber: count }; } export { atomicOrderUpdate }; ``` **Why good:** Atomic execution (no interleaving), typed results, single HTTP request ### Pipeline vs Transaction Decision ```typescript // Use PIPELINE when commands are independent // (order doesn't matter, partial failure is acceptable) const pipe = redis.pipeline(); pipe.set("analytics:page-views", views); pipe.set("analytics:unique-users", users); await pipe.exec(); // Use TRANSACTION when commands must be all-or-nothing // (atomic execution, no interleaving from other clients) const tx = redis.multi(); tx.decrby(`balance:${fromUser}`, amount); tx.incrby(`balance:${toUser}`, amount); await tx.exec(); ``` --- ## Cache-Aside Pattern ```typescript import { Redis } from "@upstash/redis"; const redis = Redis.fromEnv(); const CACHE_TTL_SECONDS = 300; async function cacheAside<T>( key: string, fetcher: () => Promise<T>, ttlSeconds: number = CACHE_TTL_SECONDS, ): Promise<T> { // Check cache first const cached = await redis.get<T>(key); if (cached !== null) { return cached; } // Cache miss -- fetch from source const data = await fetcher(); // Populate cache (fire-and-forget -- don't block response on cache write) redis.set(key, data, { ex: ttlSeconds }).catch((err) => { // Log cache write failure via your logging solution }); return data; } export { cacheAside }; ``` **Why good:** Generic `cacheAside<T>` works with any type, auto-serialization handles objects, fire-and-forget cache write, `null` check for cache miss (not truthiness) ```typescript // BAD -- blocking on cache write async function badCacheAside(key: string, fetcher: () => Promise<unknown>) { const cached = await redis.get(key); if (cached) return cached; // BUG: falsy values (0, "", false) treated as cache miss const data = await fetcher(); await redis.set(key, data); // BLOCKS response on cache write // Missing TTL -- unbounded memory growth return data; } ``` **Why bad:** Truthiness check fails for falsy values like `0` or `""`, blocking cache write adds latency to response, missing TTL causes unbounded growth --- ## SET Options (Differs from ioredis) ```typescript // Upstash: options object await redis.set("key", "value", { ex: 300 }); // TTL in seconds await redis.set("key", "value", { px: 5000 }); // TTL in milliseconds await redis.set("key", "value", { nx: true }); // Only if NOT exists await redis.set("key", "value", { xx: true }); // Only if EXISTS await redis.set("key", "value", { ex: 300, nx: true }); // Combined // ioredis: positional args (does NOT work with @upstash/redis) // redis.set("key", "value", "EX", 300); // WRONG for Upstash // redis.set("key", "value", "EX", 30, "NX"); // WRONG for Upstash ``` **Gotcha:** If migrating from ioredis, all SET option arguments must be converted to the object form. --- ## ZADD Options (Differs from ioredis) ```typescript // Upstash: object with score + member await redis.zadd("leaderboard", { score: 100, member: "player-1" }); // Multiple members await redis.zadd( "leaderboard", { score: 100, member: "player-1" }, { score: 200, member: "player-2" }, ); // ioredis: positional args (does NOT work with @upstash/redis) // redis.zadd("leaderboard", 100, "player-1"); // WRONG for Upstash ``` --- ## Singleton Pattern for Serverless ```typescript // lib/redis.ts -- module-level singleton import { Redis } from "@upstash/redis"; // Reused across warm invocations (HTTP keep-alive benefits) const redis = Redis.fromEnv(); export { redis }; ``` ```typescript // api/handler.ts -- import the singleton import { redis } from "../lib/redis"; async function handler(request: Request): Promise<Response> { const data = await redis.get("key"); return new Response(JSON.stringify(data)); } export { handler }; ``` **Why good:** Module-level singleton is reused across warm Lambda/Edge invocations, avoids creating new instances per request, benefits from HTTP keep-alive --- ## SCAN for Key Iteration ```typescript const SCAN_BATCH_SIZE = 100; let cursor = 0; const allKeys: string[] = []; do { const [nextCursor, keys] = await redis.scan(cursor, { match: "user:*", count: SCAN_BATCH_SIZE, }); cursor = nextCursor; allKeys.push(...keys); } while (cursor !== 0); ``` **Why good:** Incremental scanning, doesn't block Redis like `KEYS`, `count` is a hint for batch size --- _Full skill documentation: [SKILL.md](../SKILL.md) | Quick reference: [reference.md](../reference.md)_ -
qstash.md 5.3 KB
# Upstash -- QStash Examples > Background jobs, scheduling, message publishing, and receiver verification. Reference from [SKILL.md](../SKILL.md). **Related examples:** - [core.md](core.md) -- Redis client setup, commands, pipeline - [rate-limiting.md](rate-limiting.md) -- @upstash/ratelimit algorithms --- ## QStash Client Setup ```typescript import { Client } from "@upstash/qstash"; const qstash = new Client({ token: process.env.QSTASH_TOKEN!, }); export { qstash }; ``` **Environment variable:** `QSTASH_TOKEN` from Upstash Console. --- ## Publishing a Background Job Fire-and-forget job that QStash delivers to your API endpoint via HTTP POST. ```typescript import { Client } from "@upstash/qstash"; const qstash = new Client({ token: process.env.QSTASH_TOKEN!, }); const MAX_RETRIES = 3; async function enqueueOrderProcessing(orderId: string) { const response = await qstash.publishJSON({ url: "https://your-app.com/api/process-order", body: { orderId, action: "fulfill" }, retries: MAX_RETRIES, }); return response.messageId; // Track the message } export { enqueueOrderProcessing }; ``` **Why good:** Named constant for retries, `publishJSON` auto-serializes the body, returns `messageId` for tracking, QStash handles retries on failure **How it works:** QStash stores your message durably and POSTs it to the destination URL. If the endpoint returns a non-2xx status, QStash retries with exponential backoff up to `retries` times. This provides at-least-once delivery guarantee. --- ## Delayed Message ```typescript const DELAY_SECONDS = "30s"; await qstash.publishJSON({ url: "https://your-app.com/api/send-reminder", body: { userId: "user-123", type: "cart-abandonment" }, delay: DELAY_SECONDS, }); ``` **When to use:** Delayed notifications, cart abandonment reminders, scheduled cleanups. The message is delivered after the specified delay. --- ## Scheduled (Cron) Messages ```typescript // Send a weekly report every Monday at 9am UTC await qstash.publishJSON({ url: "https://your-app.com/api/weekly-report", body: { reportType: "usage-summary" }, cron: "0 9 * * 1", // Standard cron syntax }); ``` **When to use:** Recurring jobs (reports, cleanups, syncs) without maintaining a cron server or long-running process. --- ## Publishing to URL Groups (Fan-Out) Deliver the same message to multiple endpoints simultaneously. ```typescript // First, create a URL group via QStash API or Console // Then publish to the group by name: await qstash.publishJSON({ urlGroup: "my-topic", body: { event: "user.created", userId: "user-456" }, }); ``` **When to use:** Event fan-out to multiple services (notifications, analytics, audit logs). --- ## Receiver Verification Verify that incoming webhooks are actually from QStash (prevents spoofing). ```typescript import { Receiver } from "@upstash/qstash"; const receiver = new Receiver({ currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!, nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!, }); async function verifyQStashWebhook(request: Request): Promise<boolean> { const signature = request.headers.get("upstash-signature"); if (!signature) return false; const body = await request.text(); try { await receiver.verify({ signature, body, }); return true; } catch { return false; } } export { verifyQStashWebhook }; ``` **Why good:** Verifies webhook signature to prevent spoofing, uses both current and next signing keys for key rotation, returns boolean for clean control flow **Environment variables:** `QSTASH_CURRENT_SIGNING_KEY` and `QSTASH_NEXT_SIGNING_KEY` from QStash dashboard. --- ## Complete Handler Example ```typescript import { verifyQStashWebhook } from "../lib/qstash-verify"; async function processOrderHandler(request: Request): Promise<Response> { // Step 1: Verify the webhook is from QStash const isValid = await verifyQStashWebhook(request); if (!isValid) { return new Response("Unauthorized", { status: 401 }); } // Step 2: Parse and process the job const body = (await request.json()) as { orderId: string; action: string }; try { await fulfillOrder(body.orderId); // Return 2xx to acknowledge -- QStash will not retry return new Response("OK", { status: 200 }); } catch (error) { // Return 5xx to trigger QStash retry return new Response("Processing failed", { status: 500 }); } } async function fulfillOrder(orderId: string): Promise<void> { // Your order processing logic } export { processOrderHandler }; ``` **Key pattern:** Return 2xx to acknowledge successful processing. Return 5xx to trigger QStash's automatic retry mechanism. QStash provides at-least-once delivery -- design your handlers to be idempotent. --- ## QStash vs Direct Redis Queues ``` When should I use QStash vs Redis lists/streams for queuing? |-- Serverless (no long-running consumers)? | --> QStash -- push-based, no consumer process needed |-- Need ordering guarantees (FIFO)? | --> QStash FIFO queues or Redis Streams |-- Need scheduled/delayed delivery? | --> QStash -- built-in delay and cron support |-- Need sub-second latency? | --> Redis Streams with persistent consumer (not serverless-compatible) |-- Need fan-out to multiple endpoints? --> QStash URL Groups -- built-in fan-out ``` --- _Full skill documentation: [SKILL.md](../SKILL.md) | Quick reference: [reference.md](../reference.md)_ -
rate-limiting.md 7.1 KB
# Upstash -- Rate Limiting Examples > @upstash/ratelimit algorithms, middleware integration, analytics, and edge runtime patterns. Reference from [SKILL.md](../SKILL.md). **Related examples:** - [core.md](core.md) -- Client setup, commands, pipeline, transactions - [qstash.md](qstash.md) -- Background jobs, scheduling --- ## Sliding Window Rate Limiter Smoothest algorithm -- no burst-at-boundary issues. Recommended default. ```typescript import { Ratelimit } from "@upstash/ratelimit"; import { Redis } from "@upstash/redis"; const MAX_REQUESTS = 100; const WINDOW_DURATION = "60 s"; const ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(MAX_REQUESTS, WINDOW_DURATION), analytics: true, prefix: "ratelimit:api", }); export { ratelimit }; ``` **Why good:** Named constants for limits, analytics enabled for monitoring, custom prefix avoids key collisions, sliding window prevents boundary burst problem **When to use:** API endpoint protection, login attempt limiting, strict evenly-distributed rate limiting. --- ## Fixed Window Rate Limiter Lowest computational cost. Allows burst at window boundaries. ```typescript import { Ratelimit } from "@upstash/ratelimit"; import { Redis } from "@upstash/redis"; const MAX_REQUESTS = 50; const WINDOW_DURATION = "1 m"; const ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.fixedWindow(MAX_REQUESTS, WINDOW_DURATION), }); export { ratelimit }; ``` **When to use:** Simple rate limiting where boundary bursts are acceptable, multi-region setups (lower Redis command overhead than sliding window). **Gotcha:** A user can make `MAX_REQUESTS` at second 59 of window 1 and `MAX_REQUESTS` at second 0 of window 2, effectively doubling their rate at boundaries. --- ## Token Bucket Rate Limiter Allows controlled bursts up to bucket capacity, then refills at a steady rate. ```typescript import { Ratelimit } from "@upstash/ratelimit"; import { Redis } from "@upstash/redis"; const REFILL_RATE = 10; // tokens refilled per interval const REFILL_INTERVAL = "10 s"; const MAX_TOKENS = 50; // maximum bucket capacity const ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.tokenBucket(REFILL_RATE, REFILL_INTERVAL, MAX_TOKENS), }); export { ratelimit }; ``` **When to use:** File uploads, batch operations, APIs where burst traffic is expected but should be smoothed out. `MAX_TOKENS > REFILL_RATE` allows initial burst. **Not supported:** `MultiRegionRatelimit` does not support token bucket. --- ## Middleware Integration ### Generic Handler Pattern ```typescript import type { Ratelimit } from "@upstash/ratelimit"; const RATE_LIMIT_STATUS = 429; async function checkRateLimit( ratelimit: Ratelimit, identifier: string, waitUntil?: (promise: Promise<unknown>) => void, ): Promise<Response | null> { const { success, limit, remaining, reset, pending } = await ratelimit.limit(identifier); // CRITICAL: Handle pending promise in edge runtimes if (waitUntil) { waitUntil(pending); } if (!success) { return new Response("Too Many Requests", { status: RATE_LIMIT_STATUS, 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 null; // Request allowed } export { checkRateLimit }; ``` **Why good:** Returns `null` when allowed (caller continues), returns `Response` when blocked, handles `pending` via optional `waitUntil`, standard rate limit headers, `Retry-After` for client backoff ### Vercel Edge Usage ```typescript import { ratelimit } from "../lib/rate-limiter"; import { checkRateLimit } from "../lib/check-rate-limit"; async function handler( request: Request, context: { waitUntil: (p: Promise<unknown>) => void }, ) { const ip = request.headers.get("x-forwarded-for") ?? "anonymous"; const blocked = await checkRateLimit( ratelimit, `api:${ip}`, context.waitUntil.bind(context), ); if (blocked) return blocked; // Process request... return new Response("OK"); } export { handler }; ``` --- ## The `pending` Promise (Critical for Edge Runtimes) ```typescript const { success, pending } = await ratelimit.limit("user:123"); // BAD -- pending promise is silently dropped // Analytics data is lost, multi-region sync may fail if (!success) return new Response("Rate limited", { status: 429 }); // GOOD -- pending promise is handled // On Vercel Edge: context.waitUntil(pending); // On Cloudflare Workers: ctx.waitUntil(pending); // On Node.js servers (not edge): pending resolves naturally, no action needed ``` **Why this matters:** The `pending` promise handles async operations like analytics submission and multi-region synchronization. In edge runtimes, the runtime terminates after the response is sent -- without `waitUntil`, pending work is silently dropped. --- ## Rate Limiting by Multiple Identifiers ```typescript import { Ratelimit } from "@upstash/ratelimit"; import { Redis } from "@upstash/redis"; const IP_LIMIT = 100; const IP_WINDOW = "60 s"; const USER_LIMIT = 500; const USER_WINDOW = "60 s"; // Separate limiters for different scopes const ipLimiter = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(IP_LIMIT, IP_WINDOW), prefix: "ratelimit:ip", }); const userLimiter = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(USER_LIMIT, USER_WINDOW), prefix: "ratelimit:user", }); async function dualRateLimit( ip: string, userId: string | null, waitUntil?: (p: Promise<unknown>) => void, ): Promise<{ allowed: boolean; response?: Response }> { // Check IP limit first (cheaper, catches abuse early) const ipResult = await ipLimiter.limit(ip); if (waitUntil) waitUntil(ipResult.pending); if (!ipResult.success) { return { allowed: false, response: new Response("Rate limited", { status: 429 }), }; } // Check user limit if authenticated if (userId) { const userResult = await userLimiter.limit(userId); if (waitUntil) waitUntil(userResult.pending); if (!userResult.success) { return { allowed: false, response: new Response("Rate limited", { status: 429 }), }; } } return { allowed: true }; } export { dualRateLimit }; ``` **Why good:** Separate limiters with different limits and prefixes, IP checked first (cheapest), user checked only when authenticated --- ## Checking Remaining Tokens Without Consuming ```typescript // Check remaining quota without consuming a token const { remaining, reset } = await ratelimit.getRemaining("user:123"); if (remaining < 10) { // User approaching limit -- log or alert as appropriate } ``` **When to use:** Dashboard displays, pre-flight checks, warning thresholds. --- ## Resetting Rate Limit State ```typescript // Reset a user's rate limit (e.g., after subscription upgrade) await ratelimit.resetUsedTokens("user:123"); ``` **When to use:** Account upgrades, admin overrides, testing. --- _Full skill documentation: [SKILL.md](../SKILL.md) | Quick reference: [reference.md](../reference.md)_
-
-
reference.md 12.4 KB
# Upstash Quick Reference > Command cheat sheet, constructor options, environment variables, eviction policies, and production checklist. See [SKILL.md](SKILL.md) for core concepts and [examples/](examples/) for code examples. --- ## Constructor Options ### Redis Constructor ```typescript import { Redis } from "@upstash/redis"; // Option 1: Auto-load from environment (preferred) const redis = Redis.fromEnv(); // Option 2: Explicit configuration const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL!, token: process.env.UPSTASH_REDIS_REST_TOKEN!, automaticDeserialization: true, // default: true responseEncoding: "base64", // default: "base64" (set false to disable) enableAutoPipelining: false, // default: false enableTelemetry: true, // default: true signal: () => AbortSignal.timeout(5000), // request timeout }); ``` | Option | Default | Description | | -------------------------- | ---------- | --------------------------------------------------------------------- | | `url` | -- | REST API endpoint (`UPSTASH_REDIS_REST_URL`) | | `token` | -- | Auth token (`UPSTASH_REDIS_REST_TOKEN`) | | `automaticDeserialization` | `true` | Auto `JSON.parse` responses; set `false` for raw strings | | `responseEncoding` | `"base64"` | Request base64-encoded responses; set `false` if output looks garbled | | `enableAutoPipelining` | `false` | Batch commands from same event loop tick into single request | | `enableTelemetry` | `true` | Send anonymous usage data; disable with `UPSTASH_DISABLE_TELEMETRY=1` | | `latencyLogging` | `true` | Log per-command latency to console; set `false` to suppress | | `signal` | -- | Factory function returning `AbortSignal` for request timeouts | --- ## Environment Variables | Variable | Purpose | Required | | ---------------------------- | ------------------------------- | ------------------- | | `UPSTASH_REDIS_REST_URL` | REST API endpoint | Yes | | `UPSTASH_REDIS_REST_TOKEN` | REST API auth token | Yes | | `UPSTASH_DISABLE_TELEMETRY` | Set to `1` to disable telemetry | No | | `QSTASH_TOKEN` | QStash auth token | For QStash | | `QSTASH_CURRENT_SIGNING_KEY` | QStash webhook verification key | For QStash receiver | | `QSTASH_NEXT_SIGNING_KEY` | QStash webhook verification key | For QStash receiver | --- ## Command Quick Reference ### String Commands | Command | Upstash SDK | | ------------------------------- | --------------------------------------- | | `SET key value` | `redis.set("key", value)` | | `SET key value EX seconds` | `redis.set("key", value, { ex: 300 })` | | `SET key value PX milliseconds` | `redis.set("key", value, { px: 5000 })` | | `SET key value NX` | `redis.set("key", value, { nx: true })` | | `SET key value XX` | `redis.set("key", value, { xx: true })` | | `GET key` | `redis.get<Type>("key")` | | `GETDEL key` | `redis.getdel("key")` | | `MGET key1 key2` | `redis.mget<[T1, T2]>("k1", "k2")` | | `MSET key1 val1 key2 val2` | `redis.mset({ k1: "v1", k2: "v2" })` | | `INCR key` | `redis.incr("key")` | | `INCRBY key amount` | `redis.incrby("key", 5)` | | `DECR key` | `redis.decr("key")` | | `DECRBY key amount` | `redis.decrby("key", 5)` | | `APPEND key value` | `redis.append("key", "text")` | **Key difference from ioredis:** SET options use an object `{ ex, px, nx, xx }` -- NOT positional args like `"EX", 300`. ### Hash Commands | Command | Upstash SDK | | ---------------------- | --------------------------------------------------- | | `HSET key field value` | `redis.hset("key", { field: "value" })` | | `HGET key field` | `redis.hget<Type>("key", "field")` | | `HGETALL key` | `redis.hgetall<Record<string, T>>("key")` | | `HMGET key f1 f2` | `redis.hmget<Record<string, T>>("key", "f1", "f2")` | | `HDEL key field` | `redis.hdel("key", "field")` | | `HINCRBY key field n` | `redis.hincrby("key", "field", 1)` | | `HEXISTS key field` | `redis.hexists("key", "field")` | | `HKEYS key` | `redis.hkeys("key")` | | `HVALS key` | `redis.hvals("key")` | | `HLEN key` | `redis.hlen("key")` | ### List Commands | Command | Upstash SDK | | ----------------------- | ------------------------------- | | `LPUSH key value` | `redis.lpush("key", "value")` | | `RPUSH key value` | `redis.rpush("key", "value")` | | `LPOP key` | `redis.lpop("key")` | | `RPOP key` | `redis.rpop("key")` | | `LRANGE key start stop` | `redis.lrange("key", 0, -1)` | | `LLEN key` | `redis.llen("key")` | | `LINDEX key index` | `redis.lindex("key", 0)` | | `LSET key index value` | `redis.lset("key", 0, "value")` | | `LTRIM key start stop` | `redis.ltrim("key", 0, 99)` | ### Set Commands | Command | Upstash SDK | | ---------------------- | ---------------------------------- | | `SADD key member` | `redis.sadd("key", "member")` | | `SREM key member` | `redis.srem("key", "member")` | | `SMEMBERS key` | `redis.smembers("key")` | | `SISMEMBER key member` | `redis.sismember("key", "member")` | | `SCARD key` | `redis.scard("key")` | | `SINTER key1 key2` | `redis.sinter("key1", "key2")` | | `SUNION key1 key2` | `redis.sunion("key1", "key2")` | | `SDIFF key1 key2` | `redis.sdiff("key1", "key2")` | | `SPOP key` | `redis.spop("key")` | ### Sorted Set Commands | Command | Upstash SDK | | -------------------------- | ------------------------------------------------- | | `ZADD key score member` | `redis.zadd("key", { score: 100, member: "p1" })` | | `ZSCORE key member` | `redis.zscore("key", "member")` | | `ZRANK key member` | `redis.zrank("key", "member")` | | `ZRANGE key start stop` | `redis.zrange("key", 0, 9)` | | `ZREVRANGE key start stop` | `redis.zrange("key", 0, 9, { rev: true })` | | `ZINCRBY key incr member` | `redis.zincrby("key", 10, "member")` | | `ZREM key member` | `redis.zrem("key", "member")` | | `ZCARD key` | `redis.zcard("key")` | **Key difference from ioredis:** `ZADD` uses `{ score, member }` object -- NOT positional args. ### Key Management Commands | Command | Upstash SDK | | -------------------- | -------------------------------------- | | `DEL key` | `redis.del("key")` | | `EXISTS key` | `redis.exists("key")` | | `EXPIRE key seconds` | `redis.expire("key", 300)` | | `PEXPIRE key ms` | `redis.pexpire("key", 5000)` | | `TTL key` | `redis.ttl("key")` | | `PTTL key` | `redis.pttl("key")` | | `TYPE key` | `redis.type("key")` | | `RENAME key newkey` | `redis.rename("old", "new")` | | `SCAN cursor` | `redis.scan(0, { match: "user:*" })` | | `KEYS pattern` | `redis.keys("user:*")` (avoid in prod) | --- ## @upstash/ratelimit Response Shape ```typescript type RatelimitResponse = { success: boolean; // Request allowed (true) or rejected (false) limit: number; // Max requests per window remaining: number; // Requests left in current window reset: number; // Unix timestamp (ms) when limits reset pending: Promise<unknown>; // Async operations -- MUST handle in edge runtimes reason?: string; // "timeout" | "cacheBlock" | "denyList" | undefined deniedValue?: string; // Value from deny list if reason is "denyList" }; ``` --- ## Upstash Global Database | Aspect | Detail | | --------------------- | ----------------------------------------------------------- | | **Architecture** | 1 primary region + N read regions | | **Write routing** | All writes go to primary, replicated async to read regions | | **Read routing** | Reads served from nearest replica | | **Consistency model** | Eventually consistent (Last-Write-Wins conflict resolution) | | **Read latency** | <1ms same-region, <50ms cross-continent (p99) | | **Write cost** | Replicated to all regions = (N+1) x write commands billed | | **Best for** | Read-heavy workloads with global users | | **Avoid for** | Write-heavy workloads, strong consistency requirements | --- ## Eviction Policies Set via Upstash Console when creating the database: | Policy | Behavior | | ----------------- | ------------------------------------------------ | | `noeviction` | Return error when memory limit reached (default) | | `allkeys-lru` | Evict least recently used keys | | `allkeys-lfu` | Evict least frequently used keys | | `allkeys-random` | Evict random keys | | `volatile-lru` | Evict LRU keys that have TTL set | | `volatile-lfu` | Evict LFU keys that have TTL set | | `volatile-random` | Evict random keys that have TTL set | | `volatile-ttl` | Evict keys with shortest TTL first | **Recommendation:** Use `allkeys-lru` for caching workloads. Use `noeviction` when all data must persist (session stores). --- ## Production Checklist ### Setup - [ ] `Redis.fromEnv()` used (no hardcoded credentials) - [ ] `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` set in all environments - [ ] TTL set on all cache keys (`{ ex: seconds }` on `set()`) - [ ] Pipeline used for 3+ independent commands in a handler - [ ] Eviction policy configured in Upstash Console ### Rate Limiting - [ ] `@upstash/ratelimit` used instead of manual Lua scripts - [ ] `pending` promise handled with `context.waitUntil()` in edge runtimes - [ ] Rate limit headers set on 429 responses (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`) - [ ] Appropriate algorithm selected (sliding window for strict, fixed window for simple, token bucket for burst) ### Global Database (if used) - [ ] Primary region set closest to write-heavy services - [ ] Read regions placed near user populations - [ ] Application handles eventual consistency (no read-after-write guarantees across regions) - [ ] Write cost multiplier accounted for in budget ### Security - [ ] REST tokens not committed to version control - [ ] `UPSTASH_DISABLE_TELEMETRY=1` set if telemetry unwanted - [ ] QStash webhook signatures verified with `Receiver` --- ## Unsupported Operations (REST API Limitations) These Redis features are NOT available through `@upstash/redis`: - **Pub/Sub** (`SUBSCRIBE`, `PUBLISH`) -- requires persistent TCP connection - **Blocking commands** (`BRPOP`, `BLPOP`, `XREAD BLOCK`) -- HTTP cannot block - **Lua scripting** (`EVAL`, `EVALSHA`) -- not exposed via REST API - **WATCH** -- optimistic locking requires connection state - **CLIENT commands** -- no persistent connection to manage - **CLUSTER commands** -- managed by Upstash internally **Alternative:** Use ioredis with a TCP connection for these features. --- _Full skill documentation: [SKILL.md](SKILL.md) | Examples: [examples/](examples/)_ -
SKILL.md 18 KB
--- name: api-database-upstash description: Upstash serverless Redis -- REST-based client, auto-serialization, pipelines, rate limiting, QStash, edge compatibility, global replication --- # Upstash Patterns > **Quick Guide:** Upstash provides a **REST/HTTP-based Redis client** (`@upstash/redis`) designed for serverless and edge runtimes where TCP connections are unavailable. Unlike ioredis/node-redis, every command is an HTTP request -- no persistent connections, no connection pools, no teardown. The client **automatically serializes/deserializes JSON** (objects stored via `set` come back as objects from `get`), which is convenient but has gotchas with large numbers and cross-client compatibility. Use `redis.pipeline()` to batch commands into a single HTTP request, `redis.multi()` for atomic transactions, and `@upstash/ratelimit` for pre-built rate limiting algorithms. For background jobs, use `@upstash/qstash` which pushes messages to your API via HTTP webhooks. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST use `Redis.fromEnv()` for initialization in production code -- never hardcode `UPSTASH_REDIS_REST_URL` or `UPSTASH_REDIS_REST_TOKEN` values)** **(You MUST handle the `pending` promise from `@upstash/ratelimit` responses in edge runtimes -- use `context.waitUntil(pending)` on Vercel Edge/Cloudflare Workers or analytics data is lost)** **(You MUST use `redis.pipeline()` when issuing 3+ independent commands in a single handler -- each command is a separate HTTP round-trip without pipelining)** **(You MUST NOT use Upstash for Pub/Sub, blocking commands (BRPOP, BLPOP, XREAD BLOCK), or Lua scripting -- REST API does not support these; use ioredis with a TCP connection instead)** </critical_requirements> --- ## Examples - [Core Patterns](examples/core.md) -- Client setup, commands, auto-serialization, pipeline, transactions - [Rate Limiting](examples/rate-limiting.md) -- @upstash/ratelimit algorithms, middleware, analytics - [QStash](examples/qstash.md) -- Background jobs, scheduling, message publishing **Additional resources:** - [reference.md](reference.md) -- Command cheat sheet, constructor options, environment variables, eviction policies --- **Auto-detection:** Upstash, @upstash/redis, @upstash/ratelimit, @upstash/qstash, Redis.fromEnv, UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN, Ratelimit.slidingWindow, Ratelimit.fixedWindow, Ratelimit.tokenBucket, serverless Redis, edge Redis, REST Redis **When to use:** - Serverless functions (AWS Lambda, Vercel, Netlify) that cannot maintain TCP connections - Edge runtimes (Cloudflare Workers, Vercel Edge, Fastly Compute) that only support HTTP - Rate limiting API routes with pre-built algorithms (sliding window, fixed window, token bucket) - Caching in serverless/edge where ioredis connection pooling is impractical - Background job scheduling with QStash (push-based, no long-running consumers needed) - Global read latency optimization via Upstash Global Database with read replicas **Key patterns covered:** - `@upstash/redis` client setup with `Redis.fromEnv()` and constructor options - Automatic JSON serialization/deserialization behavior and gotchas - Pipeline batching (`redis.pipeline()`) and atomic transactions (`redis.multi()`) - `@upstash/ratelimit` algorithms: sliding window, fixed window, token bucket - `@upstash/qstash` for serverless background jobs and scheduling - Global Database architecture (primary + read regions, eventual consistency) - Edge runtime compatibility and `context.waitUntil()` patterns **When NOT to use:** - Long-running servers with persistent connections (use ioredis -- lower latency per command via TCP) - Pub/Sub, blocking commands, or Lua scripting (REST API does not support these) - Write-heavy workloads on Global Database (writes always go to primary region) - Latency-critical paths where per-command HTTP overhead (~5-15ms) is unacceptable (use ioredis with TCP for <1ms per command) - Large payloads (>1 MB) -- REST API has payload size limits --- <philosophy> ## Philosophy Upstash exists because **serverless and edge runtimes cannot maintain TCP connections**. Traditional Redis clients (ioredis, node-redis) rely on persistent TCP sockets -- they fail in Cloudflare Workers, break in short-lived Lambda functions, and cannot run in browser/WebAssembly environments. Upstash replaces TCP with REST/HTTP, trading per-command latency (~5-15ms vs <1ms) for universal compatibility. **Core principles:** 1. **Connectionless by design** -- Every command is a stateless HTTP request. No connection pools, no teardown, no connection limits. This is a feature, not a limitation. 2. **Auto-serialization is default** -- Objects go in, objects come out. No manual `JSON.stringify`/`JSON.parse`. This simplifies 90% of use cases but surprises developers who expect raw string behavior. 3. **Pipeline for performance** -- Without pipelining, N commands = N HTTP requests. Always batch independent commands with `redis.pipeline()` to reduce round-trips. 4. **Rate limiting as a first-class citizen** -- `@upstash/ratelimit` provides production-ready algorithms without writing Lua scripts. The library handles all the Redis plumbing internally. 5. **Push-based messaging** -- QStash delivers messages TO your API via HTTP webhooks. No long-running consumer processes needed -- perfect for serverless. </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: Client Setup with Redis.fromEnv() Initialize using environment variables for zero-config deployment. See [examples/core.md](examples/core.md) for full examples including constructor options and timeout configuration. ```typescript // Good Example import { Redis } from "@upstash/redis"; const redis = Redis.fromEnv(); // Reads UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN automatically export { redis }; ``` **Why good:** Zero-config, environment variables injected by platform (Vercel, Fly.io), no secrets in code ```typescript // Bad Example import { Redis } from "@upstash/redis"; const redis = new Redis({ url: "https://us1-merry-cat-12345.upstash.io", token: "AXXXAAIgcDE...", }); ``` **Why bad:** Hardcoded credentials leak in version control, non-portable across environments --- ### Pattern 2: Automatic JSON Serialization Upstash auto-serializes objects with `JSON.stringify` on write and `JSON.parse` on read. See [examples/core.md](examples/core.md) for type-safe patterns and disabling auto-serialization. ```typescript // Good Example -- objects round-trip automatically interface UserProfile { name: string; email: string; loginCount: number; } const CACHE_TTL_SECONDS = 3600; await redis.set<UserProfile>( "user:123", { name: "Alice", email: "alice@example.com", loginCount: 42, }, { ex: CACHE_TTL_SECONDS }, ); // Returns typed object -- no JSON.parse needed const user = await redis.get<UserProfile>("user:123"); // user is UserProfile | null ``` **Why good:** TypeScript generics provide type safety, no manual serialization, TTL set via options object ```typescript // Bad Example -- unnecessary manual serialization await redis.set("user:123", JSON.stringify({ name: "Alice" })); const raw = await redis.get("user:123"); const user = JSON.parse(raw as string); // Double-serialized: "{\"name\":\"Alice\"}" ``` **Why bad:** Auto-serialization already calls `JSON.stringify` -- doing it manually results in double-encoded strings that return as escaped JSON --- ### Pattern 3: Pipeline Batching Batch multiple commands into a single HTTP request. Without pipelining, each command is a separate round-trip (~5-15ms each). See [examples/core.md](examples/core.md) for typed pipeline results. ```typescript // Good Example -- single HTTP request for all commands const USER_TTL_SECONDS = 3600; const pipe = redis.pipeline(); pipe.set("user:123:name", "Alice", { ex: USER_TTL_SECONDS }); pipe.set("user:123:email", "alice@example.com", { ex: USER_TTL_SECONDS }); pipe.incr("stats:signups"); const results = await pipe.exec<["OK", "OK", number]>(); // results[0] => "OK" // results[1] => "OK" // results[2] => 1 (incremented value) ``` **Why good:** Single HTTP round-trip for 3 commands, typed results with generics, named TTL constant ```typescript // Bad Example -- 3 separate HTTP requests await redis.set("user:123:name", "Alice"); await redis.set("user:123:email", "alice@example.com"); await redis.incr("stats:signups"); // 3 round-trips = ~15-45ms total vs ~5-15ms with pipeline ``` **Why bad:** Each `await` is a separate HTTP request, tripling latency in serverless where every millisecond of cold start matters --- ### Pattern 4: Atomic Transactions Use `redis.multi()` when commands must execute atomically. See [examples/core.md](examples/core.md) for examples. ```typescript // Good Example -- atomic counter + flag update const tx = redis.multi(); tx.incr("order:count"); tx.set("order:last-updated", Date.now()); const [count, status] = await tx.exec<[number, "OK"]>(); ``` **Why good:** All commands execute atomically (no interleaving from other clients), typed results **When to use pipeline vs transaction:** - **Pipeline** (`redis.pipeline()`) -- Commands are independent, you want batching for speed, atomicity not required - **Transaction** (`redis.multi()`) -- Commands must all succeed together, no interleaving allowed --- ### Pattern 5: Rate Limiting with @upstash/ratelimit Pre-built rate limiting that handles all Redis internals. See [examples/rate-limiting.md](examples/rate-limiting.md) for all algorithms, middleware integration, and analytics. ```typescript // Good Example import { Ratelimit } from "@upstash/ratelimit"; import { Redis } from "@upstash/redis"; const MAX_REQUESTS = 10; const WINDOW_DURATION = "10 s"; const ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(MAX_REQUESTS, WINDOW_DURATION), analytics: true, }); const { success, limit, remaining, reset, pending } = await ratelimit.limit("user:123"); // CRITICAL: In edge runtimes, handle the pending promise // context.waitUntil(pending); if (!success) { return new Response("Too Many Requests", { status: 429, headers: { "X-RateLimit-Limit": String(limit), "X-RateLimit-Remaining": String(remaining), "X-RateLimit-Reset": String(reset), }, }); } ``` **Why good:** No Lua scripts needed, named constants for limits, analytics for monitoring, proper 429 response with standard headers --- ### Pattern 6: QStash Background Jobs Push-based messaging for serverless. See [examples/qstash.md](examples/qstash.md) for scheduling, retries, and receiver verification. ```typescript // Good Example -- publish a background job import { Client } from "@upstash/qstash"; const qstash = new Client({ token: process.env.QSTASH_TOKEN!, }); await qstash.publishJSON({ url: "https://your-app.com/api/process-order", body: { orderId: "order-456", action: "fulfill" }, retries: 3, delay: "10s", }); ``` **Why good:** Fire-and-forget from handler, automatic retries on failure, configurable delay, at-least-once delivery guaranteed </patterns> --- <decision_framework> ## Decision Framework ### Upstash vs ioredis/node-redis ``` Which Redis client should I use? |-- Running in edge runtime (Cloudflare Workers, Vercel Edge)? | --> @upstash/redis (only option -- no TCP available) |-- Running in serverless (Lambda, Vercel Serverless)? | |-- Short-lived functions with no connection reuse? | | --> @upstash/redis (no connection management overhead) | |-- Long-lived functions with connection pooling? | --> ioredis (lower per-command latency) |-- Running on a persistent server (Docker, EC2, K8s)? | --> ioredis (persistent TCP = <1ms latency vs ~5-15ms HTTP) |-- Need Pub/Sub, blocking commands, or Lua scripts? | --> ioredis (REST API cannot support these) |-- Need to run in browser or WebAssembly? --> @upstash/redis (HTTP works everywhere) ``` ### Which Rate Limiting Algorithm? ``` Which @upstash/ratelimit algorithm should I use? |-- Need strict, evenly distributed limiting? | --> slidingWindow -- smoothest, no burst-at-boundary issues |-- Need simple, low-overhead limiting? | --> fixedWindow -- cheapest computationally, allows boundary bursts |-- Need to allow burst traffic up to a capacity? | --> tokenBucket -- smooths bursts, allows initial spike up to maxTokens |-- Need multi-region rate limiting? --> fixedWindow (slidingWindow has high Redis command overhead in multi-region) ``` ### Pipeline vs Transaction vs Sequential ``` How should I batch these Redis commands? |-- Commands are independent (no ordering dependency)? | --> Pipeline (redis.pipeline()) -- non-atomic but single HTTP request |-- Commands must execute atomically (all-or-nothing)? | --> Transaction (redis.multi()) -- atomic, single HTTP request |-- Only 1-2 commands? --> Sequential is fine -- pipeline overhead not worth it ``` ### Global Database vs Regional ``` Should I use Upstash Global Database? |-- Read-heavy workload with users worldwide? | --> Global Database -- reads from nearest replica |-- Write-heavy workload? | --> Regional Database -- writes always go to primary, replication doubles write cost |-- Need strong consistency? | --> Regional Database -- Global is eventually consistent |-- Latency-sensitive reads from multiple continents? --> Global Database -- sub-1ms reads from nearest region ``` </decision_framework> --- <red_flags> ## RED FLAGS **High Priority Issues:** - Using `JSON.stringify()` before passing objects to `redis.set()` -- auto-serialization already handles this, resulting in double-encoded strings like `"{\"name\":\"Alice\"}"` that break on read - Ignoring the `pending` promise from `ratelimit.limit()` in edge runtimes -- analytics data and multi-region sync are lost silently; use `context.waitUntil(pending)` - Issuing 5+ sequential `await redis.get/set()` calls without pipelining -- each is a separate HTTP request, adding 25-75ms of unnecessary latency - Attempting Pub/Sub (`redis.subscribe`), blocking commands (`BRPOP`, `BLPOP`), or Lua scripting (`eval`) -- Upstash REST API does not support these; use ioredis with TCP **Medium Priority Issues:** - Missing TTL on cached keys -- same as any Redis: unbounded memory growth until eviction kicks in - Using Global Database for write-heavy workloads -- writes always route to primary region and replication doubles command costs - Not setting `automaticDeserialization: false` when interoperating with non-Upstash clients -- other clients store raw strings, Upstash will fail to parse them as JSON - Creating a new `Redis` instance per request instead of reusing a module-level singleton -- while connectionless, the client still benefits from HTTP keep-alive and warm connections **Common Mistakes:** - Expecting `redis.get()` to return a string when an object was stored -- auto-deserialization returns the original object type, not a JSON string - Assuming pipeline execution is atomic -- pipelines batch for network efficiency but other clients can interleave; use `redis.multi()` for atomicity - Using `Ratelimit.slidingWindow` with `MultiRegionRatelimit` -- sliding window has high Redis command overhead in multi-region setups; use `fixedWindow` instead - Storing values larger than 1 MB -- REST API has payload size limits; store references and fetch large data from object storage **Gotchas & Edge Cases:** - **Large numbers become strings**: JavaScript cannot safely handle numbers > `2^53 - 1` (Number.MAX_SAFE_INTEGER). Upstash returns these as strings even when the TypeScript type says `number`. Always validate large numeric values. - **Base64 encoding by default**: The SDK requests base64-encoded responses to handle edge cases. If you see garbled output like `dmFsdWU=`, the response encoding is interfering -- check `responseEncoding` option. - **`redis.get()` returns `null` for missing keys, not `undefined`**: This matters for TypeScript narrowing -- check `result !== null`, not truthiness. - **SET options use an object, not positional args**: Upstash uses `redis.set("key", "value", { ex: 300 })` not `redis.set("key", "value", "EX", 300)` -- the ioredis positional argument style does not work. - **Global Database is eventually consistent**: A write followed immediately by a read from a different region may return stale data. Design for eventual consistency or use regional database for strong consistency. - **`hgetall` returns an empty object `{}` for non-existent keys**: Check `Object.keys(result).length === 0`, not `result === null`. - **`blockUntilReady()` does not work on Cloudflare Workers**: Cloudflare's `Date.now()` behaves differently; use `limit()` with manual retry logic instead. - **No WATCH command**: Upstash REST API does not support `WATCH` for optimistic locking. Use `redis.multi()` for atomic operations or implement application-level optimistic concurrency. - **Auto-pipelining is available**: The SDK can automatically batch commands issued during the same event loop tick via `enableAutoPipelining: true` in the constructor. </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST use `Redis.fromEnv()` for initialization in production code -- never hardcode `UPSTASH_REDIS_REST_URL` or `UPSTASH_REDIS_REST_TOKEN` values)** **(You MUST handle the `pending` promise from `@upstash/ratelimit` responses in edge runtimes -- use `context.waitUntil(pending)` on Vercel Edge/Cloudflare Workers or analytics data is lost)** **(You MUST use `redis.pipeline()` when issuing 3+ independent commands in a single handler -- each command is a separate HTTP round-trip without pipelining)** **(You MUST NOT use Upstash for Pub/Sub, blocking commands (BRPOP, BLPOP, XREAD BLOCK), or Lua scripting -- REST API does not support these; use ioredis with a TCP connection instead)** **Failure to follow these rules will cause credential leaks, silent data loss in edge runtimes, unnecessary latency from sequential HTTP requests, and runtime errors from unsupported commands.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.