Claude Skill

api-database-upstash

Upstash serverless Redis -- REST-based client, auto-serialization, pipelines, rate limiting, QStash, edge compatibility, global replication

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_api-database-upstash_skills_api-database-upstash-3a51ef5.zip · 19 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-database-upstash/skills/api-database-upstash
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git 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 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 -- 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/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



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

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.

No comments yet.

Reviews (0)

No reviews yet.

Related