Claude Skill

api-database-vercel-kv

Serverless Redis-compatible key-value store via Upstash REST API -- edge-compatible, automatic JSON serialization, TTL-based caching

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-vercel-kv_skills_api-database-vercel-kv-3a51ef5.zip · 10 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-vercel-kv/skills/api-database-vercel-kv
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

Vercel KV / Upstash Redis Patterns

Quick Guide: Use @upstash/redis (the successor to @vercel/kv) for serverless, edge-compatible Redis via REST API. Key gotchas: REST adds ~5-15ms latency per call vs TCP Redis, all values are auto-serialized as JSON (objects round-trip transparently but Date objects become strings), pipeline/multi execute as single HTTP requests but pipeline is NOT atomic. Use Redis.fromEnv() for automatic connection. Always set TTLs -- serverless Redis is billed per command.


<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 @upstash/redis for new projects -- @vercel/kv was deprecated in December 2024 and all stores were migrated to Upstash Redis)

(You MUST set TTLs on all cached data -- serverless Redis is billed per command and has storage limits per plan)

(You MUST understand that this is a REST/HTTP client, NOT a TCP Redis client -- each command is an HTTP request with ~5-15ms overhead, so batch with pipelines when possible)

</critical_requirements>


Examples

  • Core Patterns -- Client setup, CRUD operations, TTL, hashes, pipelines, transactions, rate limiting, sessions

Additional resources:

  • reference.md -- Command quick reference, environment variables, plan limits

Auto-detection: Vercel KV, @vercel/kv, @upstash/redis, Upstash Redis, KV_REST_API_URL, KV_REST_API_TOKEN, UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN, Redis.fromEnv, kv.set, kv.get, kv.hset, kv.hget, kv.incr, kv.expire, kv.del, createClient, automaticDeserialization, edge Redis, serverless Redis

When to use:

  • Caching API responses or database queries in Vercel serverless/edge functions
  • Rate limiting at the edge (sliding window counters)
  • Session storage for serverless applications
  • Feature flags, A/B test assignments, or short-lived counters
  • Any Redis use case on Vercel where TCP connections are unavailable (edge runtime)

Key patterns covered:

  • Client initialization (Redis.fromEnv(), new Redis())
  • Basic CRUD with automatic JSON serialization
  • TTL and expiration strategies
  • Hash operations for structured data
  • Pipelines (batched HTTP) and transactions (atomic MULTI/EXEC)
  • Rate limiting with sorted sets
  • Session storage patterns

When NOT to use:

  • High-throughput, low-latency Redis workloads (use ioredis with TCP -- REST adds per-request overhead)
  • Pub/Sub subscribers (REST is request-response, not persistent connections)
  • Redis Streams consumers (requires TCP client like ioredis)
  • Large value storage (>1 MB per record on free tier, billed by command count)
  • Primary database (Redis is a cache/ephemeral store, not a source of truth)



<decision_framework>

Decision Framework

Upstash Redis vs ioredis/node-redis?

Which Redis client should I use?
+-- Running in Vercel Edge Runtime? -> @upstash/redis (only option -- no TCP)
+-- Running in Vercel Serverless Functions? -> @upstash/redis (simpler) or ioredis (if you need TCP features)
+-- Need Pub/Sub subscribers? -> ioredis (REST cannot maintain subscriptions)
+-- Need Redis Streams consumers? -> ioredis (requires persistent TCP connection)
+-- Need lowest possible latency (<1ms)? -> ioredis with TCP (REST adds HTTP overhead)
+-- Simple caching/sessions/counters? -> @upstash/redis (zero connection management)

Pipeline vs Transaction vs Sequential?

How should I batch commands?
+-- Need atomicity (all-or-nothing)? -> redis.multi() (transaction)
+-- Just reducing HTTP round-trips? -> redis.pipeline() (non-atomic batch)
+-- Single independent command? -> Direct call (redis.set, redis.get, etc.)

</decision_framework>


<red_flags>

RED FLAGS

High Priority Issues:

  • Using @vercel/kv in new projects -- deprecated December 2024, use @upstash/redis instead
  • Missing TTLs on cached keys -- causes unbounded storage growth and unexpected billing
  • Manual JSON.stringify/JSON.parse with Upstash Redis -- causes double-serialization because the SDK auto-serializes all values
  • Assuming pipeline commands are atomic -- pipelines batch for HTTP efficiency but do NOT guarantee atomicity (use multi() for atomic execution)

Medium Priority Issues:

  • Making sequential Redis calls where a pipeline would work -- each call is a separate HTTP round-trip (~5-15ms each)
  • Storing values >1 MB -- REST requests have size limits per plan (100 MB max on free/pay-as-you-go, but large values degrade performance)
  • Using Upstash Redis as a primary database -- it's a cache/ephemeral store, always have a source of truth elsewhere

Common Mistakes:

  • Expecting hgetall to return an empty object {} for missing keys -- Upstash returns null (unlike ioredis which returns {})
  • Forgetting that get() returns null (not undefined) for missing keys
  • Passing Date objects and expecting them to survive round-trip -- they serialize to ISO strings and come back as strings, not Date instances

Gotchas & Edge Cases:

  • automaticDeserialization: false breaks many TypeScript types -- only disable if you need raw string responses and are prepared to handle typing manually
  • set with ex option resets TTL on overwrite (standard Redis behavior) -- if you set a key that already has a TTL, the new ex value replaces it
  • REST latency is per-request, not per-command -- a pipeline with 10 commands has the same HTTP overhead as a single command (one round-trip)
  • Free tier is limited to 500K commands/month and 256 MB storage -- monitor usage in production
  • nx (set-if-not-exists) returns null on failure, "OK" on success -- check the return value explicitly

</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 @upstash/redis for new projects -- @vercel/kv was deprecated in December 2024 and all stores were migrated to Upstash Redis)

(You MUST set TTLs on all cached data -- serverless Redis is billed per command and has storage limits per plan)

(You MUST understand that this is a REST/HTTP client, NOT a TCP Redis client -- each command is an HTTP request with ~5-15ms overhead, so batch with pipelines when possible)

Failure to follow these rules will cause deprecated package usage, unbounded storage costs, and unnecessary latency in serverless functions.

</critical_reminders>

Files (skills)
  • examples
    • core.md 11 KB
      # Vercel KV / Upstash Redis -- Core Examples
      
      > Essential patterns for serverless Redis via `@upstash/redis`. See [SKILL.md](../SKILL.md) for decision guidance and [reference.md](../reference.md) for command quick reference.
      
      ---
      
      ## Client Setup
      
      ### Environment-Based (Preferred on Vercel)
      
      ```typescript
      import { Redis } from "@upstash/redis";
      
      // Reads UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN from process.env
      const redis = Redis.fromEnv();
      
      export { redis };
      ```
      
      **Why good:** Zero-config on Vercel (env vars injected by Upstash integration), no secrets in code, single shared instance
      
      ### Explicit Configuration
      
      ```typescript
      import { Redis } from "@upstash/redis";
      
      function createRedisClient(): Redis {
        const url = process.env.UPSTASH_REDIS_REST_URL;
        const token = process.env.UPSTASH_REDIS_REST_TOKEN;
      
        if (!url || !token) {
          throw new Error(
            "UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN are required",
          );
        }
      
        return new Redis({ url, token });
      }
      
      export { createRedisClient };
      ```
      
      **Why good:** Validates env vars before use, works outside Vercel, factory pattern for testability
      
      ### Disabling Auto-Deserialization
      
      ```typescript
      import { Redis } from "@upstash/redis";
      
      // Returns raw strings instead of parsed JSON
      const redis = new Redis({
        url: process.env.UPSTASH_REDIS_REST_URL!,
        token: process.env.UPSTASH_REDIS_REST_TOKEN!,
        automaticDeserialization: false,
      });
      ```
      
      **When to use:** Only when you need raw string responses (e.g., interoperating with non-JSON data written by another client). Breaks many TypeScript types -- handle with care.
      
      ---
      
      ## Basic CRUD with Auto-Serialization
      
      ```typescript
      import { Redis } from "@upstash/redis";
      
      const redis = Redis.fromEnv();
      
      // Strings
      await redis.set("greeting", "hello");
      const greeting = await redis.get<string>("greeting"); // "hello"
      
      // Objects are auto-serialized to JSON
      interface Product {
        id: string;
        name: string;
        price: number;
      }
      
      const PRODUCT_TTL_SECONDS = 3600;
      
      await redis.set(
        "product:abc",
        { id: "abc", name: "Widget", price: 29.99 } satisfies Product,
        { ex: PRODUCT_TTL_SECONDS },
      );
      
      const product = await redis.get<Product>("product:abc");
      // product is Product | null -- fully deserialized, typed
      
      // Delete
      await redis.del("product:abc");
      
      // Check existence
      const exists = await redis.exists("product:abc"); // 0 or 1
      ```
      
      **Why good:** Type parameter on `get<T>()` provides TypeScript safety, auto-serialization handles JSON round-trips, TTL prevents stale data
      
      ---
      
      ## Gotcha: Date Objects Do Not Survive Round-Trip
      
      ```typescript
      // Bad Example -- Date becomes a string
      interface Event {
        name: string;
        date: Date;
      }
      
      await redis.set("event:1", { name: "Launch", date: new Date() });
      const event = await redis.get<Event>("event:1");
      // event.date is a STRING like "2025-03-20T..." -- NOT a Date object
      // event.date.getTime() will throw TypeError
      ```
      
      **Why bad:** `JSON.stringify(new Date())` produces an ISO string, `JSON.parse` does not revive it back to a `Date`. Store timestamps as numbers instead:
      
      ```typescript
      // Good Example -- Use epoch milliseconds for dates
      interface Event {
        name: string;
        dateMs: number;
      }
      
      await redis.set("event:1", { name: "Launch", dateMs: Date.now() });
      const event = await redis.get<Event>("event:1");
      const date = new Date(event!.dateMs); // Reconstruct Date from number
      ```
      
      ---
      
      ## TTL Strategies
      
      ```typescript
      const TTL_SHORT_SECONDS = 60; // 1 minute -- volatile/real-time data
      const TTL_MEDIUM_SECONDS = 300; // 5 minutes -- API response cache
      const TTL_LONG_SECONDS = 3600; // 1 hour -- user profiles, product data
      const TTL_SESSION_SECONDS = 86400; // 24 hours -- sessions
      
      // Set with TTL (preferred -- atomic)
      await redis.set("cache:feed", feedData, { ex: TTL_MEDIUM_SECONDS });
      
      // Set with millisecond TTL
      await redis.set("cache:realtime", data, { px: 500 });
      
      // Distributed lock: set only if not exists
      const LOCK_TTL_SECONDS = 30;
      const lockAcquired = await redis.set("lock:checkout:789", "owner-id", {
        ex: LOCK_TTL_SECONDS,
        nx: true,
      });
      // lockAcquired is "OK" if acquired, null if already held
      
      // Update TTL on existing key
      await redis.expire("cache:feed", TTL_LONG_SECONDS);
      
      // Check remaining TTL
      const remaining = await redis.ttl("cache:feed"); // seconds, -1 if no TTL, -2 if key missing
      ```
      
      ---
      
      ## Hash Operations
      
      Hashes allow partial reads/writes without serializing entire objects.
      
      ```typescript
      const USER_KEY_PREFIX = "user:";
      const USER_TTL_SECONDS = 3600;
      
      // Set multiple fields
      await redis.hset(`${USER_KEY_PREFIX}123`, {
        name: "Alice",
        email: "alice@example.com",
        loginCount: "0", // Hash values are strings in Redis
      });
      
      // Read single field
      const email = await redis.hget<string>(`${USER_KEY_PREFIX}123`, "email");
      
      // Read all fields
      const user = await redis.hgetall<Record<string, string>>(
        `${USER_KEY_PREFIX}123`,
      );
      // Returns null if key doesn't exist (unlike ioredis which returns {})
      
      // Check for missing key
      if (user === null) {
        // Key does not exist
      }
      
      // Atomic increment (useful for counters)
      await redis.hincrby(`${USER_KEY_PREFIX}123`, "loginCount", 1);
      
      // Delete specific fields
      await redis.hdel(`${USER_KEY_PREFIX}123`, "email");
      
      // Set TTL on the hash key
      await redis.expire(`${USER_KEY_PREFIX}123`, USER_TTL_SECONDS);
      ```
      
      **Why good:** Partial field reads avoid transferring entire object, `hincrby` is atomic, explicit TTL
      
      ---
      
      ## Pipelines (Non-Atomic Batch)
      
      Batch multiple commands into a single HTTP request to reduce latency.
      
      ```typescript
      const CACHE_TTL_SECONDS = 300;
      
      // Pipeline -- commands are NOT atomic but execute in a single HTTP round-trip
      const pipe = redis.pipeline();
      pipe.set("user:1:name", "Alice", { ex: CACHE_TTL_SECONDS });
      pipe.set("user:1:email", "alice@example.com", { ex: CACHE_TTL_SECONDS });
      pipe.incr("stats:signups");
      pipe.get<string>("config:feature-flag");
      
      const [setResult1, setResult2, signupCount, featureFlag] =
        await pipe.exec<[string, string, number, string | null]>();
      ```
      
      **Why good:** Single HTTP request regardless of command count, typed results via generic parameter, massive latency reduction
      
      ---
      
      ## Transactions (Atomic MULTI/EXEC)
      
      Atomic execution -- all commands succeed or all fail.
      
      ```typescript
      // Transfer balance atomically
      const tx = redis.multi();
      tx.decrby("balance:user1", 100);
      tx.incrby("balance:user2", 100);
      
      const [newBalance1, newBalance2] = await tx.exec<[number, number]>();
      ```
      
      **Why good:** Atomic -- no interleaving between commands, single HTTP request
      
      **Important:** Upstash REST transactions do NOT support `WATCH` for optimistic locking. If you need conditional updates based on current values, use Lua scripts or redesign with atomic commands (`incr`, `setnx`).
      
      ---
      
      ## Rate Limiting (Sliding Window)
      
      ```typescript
      const RATE_LIMIT_WINDOW_MS = 60_000; // 1 minute
      const RATE_LIMIT_MAX_REQUESTS = 100;
      const RATE_LIMIT_KEY_PREFIX = "ratelimit:";
      
      interface RateLimitResult {
        limited: boolean;
        remaining: number;
      }
      
      async function checkRateLimit(identifier: string): Promise<RateLimitResult> {
        const key = `${RATE_LIMIT_KEY_PREFIX}${identifier}`;
        const now = Date.now();
        const windowStart = now - RATE_LIMIT_WINDOW_MS;
      
        const pipe = redis.pipeline();
        pipe.zremrangebyscore(key, 0, windowStart); // Remove expired entries
        pipe.zcard(key); // Count current window
        pipe.zadd(key, { score: now, member: `${now}-${Math.random()}` }); // Add request
        pipe.expire(key, Math.ceil(RATE_LIMIT_WINDOW_MS / 1000)); // Auto-cleanup
      
        const results = await pipe.exec<[number, number, number, number]>();
        const currentCount = results[1];
      
        return {
          limited: currentCount >= RATE_LIMIT_MAX_REQUESTS,
          remaining: Math.max(0, RATE_LIMIT_MAX_REQUESTS - currentCount),
        };
      }
      
      export { checkRateLimit };
      export type { RateLimitResult };
      ```
      
      **Why good:** Sliding window via sorted set scores, pipeline batches all ops into one HTTP call, TTL auto-cleans abandoned keys, returns remaining quota for response headers
      
      **When to use:** Simple per-identifier rate limiting. For production workloads, consider `@upstash/ratelimit` which provides built-in sliding window, fixed window, and token bucket algorithms with less code.
      
      ---
      
      ## Session Storage
      
      ```typescript
      import { Redis } from "@upstash/redis";
      import { randomUUID } from "node:crypto";
      
      const redis = Redis.fromEnv();
      
      const SESSION_TTL_SECONDS = 86400; // 24 hours
      const SESSION_KEY_PREFIX = "session:";
      
      interface SessionData {
        userId: string;
        role: string;
        createdAt: number;
      }
      
      async function createSession(userId: string, role: string): Promise<string> {
        const sessionId = randomUUID();
        const session: SessionData = {
          userId,
          role,
          createdAt: Date.now(),
        };
      
        await redis.set(`${SESSION_KEY_PREFIX}${sessionId}`, session, {
          ex: SESSION_TTL_SECONDS,
        });
      
        return sessionId;
      }
      
      async function getSession(sessionId: string): Promise<SessionData | null> {
        return redis.get<SessionData>(`${SESSION_KEY_PREFIX}${sessionId}`);
      }
      
      async function refreshSession(sessionId: string): Promise<boolean> {
        // Extend TTL on access (sliding expiration)
        const result = await redis.expire(
          `${SESSION_KEY_PREFIX}${sessionId}`,
          SESSION_TTL_SECONDS,
        );
        return result === 1; // 1 = key exists and TTL set, 0 = key not found
      }
      
      async function destroySession(sessionId: string): Promise<void> {
        await redis.del(`${SESSION_KEY_PREFIX}${sessionId}`);
      }
      
      export { createSession, getSession, refreshSession, destroySession };
      export type { SessionData };
      ```
      
      **Why good:** Named constants, typed session data, sliding expiration via `expire()`, auto-serialization handles JSON, cleanup via TTL
      
      ---
      
      ## Cache-Aside Helper
      
      ```typescript
      const DEFAULT_CACHE_TTL_SECONDS = 300;
      
      async function cacheAside<T>(
        key: string,
        fetcher: () => Promise<T>,
        ttlSeconds: number = DEFAULT_CACHE_TTL_SECONDS,
      ): Promise<T> {
        const cached = await redis.get<T>(key);
        if (cached !== null) {
          return cached;
        }
      
        const data = await fetcher();
      
        // Fire-and-forget -- don't block response on cache write
        redis.set(key, data, { ex: ttlSeconds }).catch((err: unknown) => {
          console.error(`Cache write failed for ${key}:`, err);
        });
      
        return data;
      }
      
      export { cacheAside };
      ```
      
      **Why good:** Generic type preserves TypeScript safety, fire-and-forget prevents cache failures from blocking, auto-serialization handles objects, configurable TTL
      
      ---
      
      ## Edge Runtime Usage
      
      ```typescript
      // Works in Vercel Edge Runtime (no TCP, pure HTTP)
      // Works in any edge/serverless runtime -- export runtime config per your framework
      import { Redis } from "@upstash/redis";
      
      export const runtime = "edge";
      
      const redis = Redis.fromEnv();
      
      export async function GET(request: Request) {
        const url = new URL(request.url);
        const key = url.searchParams.get("key");
      
        if (!key) {
          return new Response("Missing key", { status: 400 });
        }
      
        const value = await redis.get(key);
        return Response.json({ value });
      }
      ```
      
      **Why good:** Works in edge runtime where TCP-based Redis clients (ioredis, node-redis) cannot connect, `Redis.fromEnv()` auto-configures
      
      ---
      
      _Full skill documentation: [SKILL.md](../SKILL.md) | Quick reference: [reference.md](../reference.md)_
      
  • reference.md 7 KB
    # Vercel KV / Upstash Redis Quick Reference
    
    > Command reference, environment variables, plan limits, and migration guide. See [SKILL.md](SKILL.md) for core concepts and [examples/core.md](examples/core.md) for code examples.
    
    ---
    
    ## Environment Variables
    
    | Variable                   | Source                               | Description                                         |
    | -------------------------- | ------------------------------------ | --------------------------------------------------- |
    | `UPSTASH_REDIS_REST_URL`   | Upstash console / Vercel integration | REST API endpoint (`https://<name>.upstash.io`)     |
    | `UPSTASH_REDIS_REST_TOKEN` | Upstash console / Vercel integration | Bearer token for REST API authentication            |
    | `KV_REST_API_URL`          | Legacy `@vercel/kv`                  | Deprecated -- migrate to `UPSTASH_REDIS_REST_URL`   |
    | `KV_REST_API_TOKEN`        | Legacy `@vercel/kv`                  | Deprecated -- migrate to `UPSTASH_REDIS_REST_TOKEN` |
    
    **On Vercel:** These are injected automatically when you install the Upstash Redis integration from the Marketplace.
    
    **Locally:** Copy from Upstash console to `.env.local` or use `vercel env pull`.
    
    ---
    
    ## Command Quick Reference
    
    ### String Commands
    
    | Method                         | Description                   | Example                              |
    | ------------------------------ | ----------------------------- | ------------------------------------ |
    | `redis.set(key, value, opts?)` | Set value (auto-serialized)   | `redis.set("k", obj, { ex: 300 })`   |
    | `redis.get<T>(key)`            | Get value (auto-deserialized) | `redis.get<User>("k")`               |
    | `redis.setex(key, ttl, value)` | Set with TTL (seconds)        | `redis.setex("k", 300, obj)`         |
    | `redis.setnx(key, value)`      | Set only if not exists        | `redis.setnx("k", obj)`              |
    | `redis.mget<T>(keys...)`       | Get multiple keys             | `redis.mget("k1", "k2")`             |
    | `redis.mset(pairs)`            | Set multiple keys             | `redis.mset({ k1: "v1", k2: "v2" })` |
    | `redis.incr(key)`              | Increment by 1                | `redis.incr("counter")`              |
    | `redis.incrby(key, n)`         | Increment by n                | `redis.incrby("counter", 5)`         |
    | `redis.decr(key)`              | Decrement by 1                | `redis.decr("counter")`              |
    | `redis.del(keys...)`           | Delete keys                   | `redis.del("k1", "k2")`              |
    
    ### Hash Commands
    
    | Method                         | Description        | Example                             |
    | ------------------------------ | ------------------ | ----------------------------------- |
    | `redis.hset(key, fields)`      | Set hash fields    | `redis.hset("u:1", { name: "A" })`  |
    | `redis.hget<T>(key, field)`    | Get hash field     | `redis.hget("u:1", "name")`         |
    | `redis.hgetall<T>(key)`        | Get all fields     | `redis.hgetall("u:1")`              |
    | `redis.hdel(key, fields...)`   | Delete fields      | `redis.hdel("u:1", "age")`          |
    | `redis.hincrby(key, field, n)` | Increment field    | `redis.hincrby("u:1", "visits", 1)` |
    | `redis.hexists(key, field)`    | Check field exists | `redis.hexists("u:1", "name")`      |
    
    ### Sorted Set Commands
    
    | Method                                  | Description     | Example                                          |
    | --------------------------------------- | --------------- | ------------------------------------------------ |
    | `redis.zadd(key, { score, member })`    | Add with score  | `redis.zadd("lb", { score: 100, member: "p1" })` |
    | `redis.zrange(key, start, stop)`        | Get range (asc) | `redis.zrange("lb", 0, 9)`                       |
    | `redis.zrem(key, members...)`           | Remove members  | `redis.zrem("lb", "p1")`                         |
    | `redis.zcard(key)`                      | Get count       | `redis.zcard("lb")`                              |
    | `redis.zremrangebyscore(key, min, max)` | Remove by score | `redis.zremrangebyscore("lb", 0, cutoff)`        |
    
    ### Key Management
    
    | Method                       | Description                 | Example                           |
    | ---------------------------- | --------------------------- | --------------------------------- |
    | `redis.expire(key, seconds)` | Set TTL                     | `redis.expire("k", 300)`          |
    | `redis.ttl(key)`             | Get remaining TTL           | `redis.ttl("k")`                  |
    | `redis.exists(keys...)`      | Check existence             | `redis.exists("k1", "k2")`        |
    | `redis.keys(pattern)`        | Find keys (caution in prod) | `redis.keys("user:*")`            |
    | `redis.scan(cursor, opts?)`  | Iterate keys safely         | `redis.scan(0, { match: "u:*" })` |
    
    ---
    
    ## Pipeline and Transaction API
    
    ```typescript
    // Pipeline (non-atomic batch -- single HTTP request)
    const pipe = redis.pipeline();
    pipe.set("k1", "v1");
    pipe.get("k2");
    const results = await pipe.exec<[string, string | null]>();
    
    // Transaction (atomic MULTI/EXEC -- single HTTP request)
    const tx = redis.multi();
    tx.decrby("balance:a", 100);
    tx.incrby("balance:b", 100);
    const results = await tx.exec<[number, number]>();
    ```
    
    ---
    
    ## Client Configuration Options
    
    ```typescript
    const redis = new Redis({
      url: "...", // REST API URL (required)
      token: "...", // Auth token (required)
      automaticDeserialization: true, // Default: true -- auto JSON parse responses
      enableTelemetry: false, // Default: true -- anonymous usage stats
    });
    ```
    
    ---
    
    ## Plan Limits (Upstash)
    
    | Limit             | Free   | Pay-as-you-go | Fixed plans     |
    | ----------------- | ------ | ------------- | --------------- |
    | Monthly commands  | 500K   | Per-command   | Unlimited       |
    | Max storage       | 256 MB | 100 GB        | 250 MB - 500 GB |
    | Max request size  | 10 MB  | 10 MB         | Up to 100 MB    |
    | Max record size   | 100 MB | 100 MB        | Up to 5 GB      |
    | Max ops/sec       | 10,000 | 10,000        | 10,000 - 16,000 |
    | Monthly bandwidth | 10 GB  | 200 GB free   | 50 GB - 20 TB   |
    | Databases (free)  | 10     | 10            | Plan-specific   |
    
    ---
    
    ## Migration from @vercel/kv to @upstash/redis
    
    ### Package swap
    
    ```bash
    npm uninstall @vercel/kv
    npm install @upstash/redis
    ```
    
    ### Code changes
    
    ```typescript
    // Before (@vercel/kv)
    import { kv } from "@vercel/kv";
    await kv.set("key", value);
    const data = await kv.get("key");
    
    // After (@upstash/redis)
    import { Redis } from "@upstash/redis";
    const redis = Redis.fromEnv();
    await redis.set("key", value);
    const data = await redis.get("key");
    ```
    
    ### Environment variable changes
    
    | Old (Vercel KV)     | New (Upstash)              |
    | ------------------- | -------------------------- |
    | `KV_REST_API_URL`   | `UPSTASH_REDIS_REST_URL`   |
    | `KV_REST_API_TOKEN` | `UPSTASH_REDIS_REST_TOKEN` |
    
    **API compatibility:** The command API is identical -- `@vercel/kv` was a thin wrapper around `@upstash/redis`. Only the import, client initialization, and env var names change.
    
    ---
    
    _Full skill documentation: [SKILL.md](SKILL.md) | Examples: [examples/core.md](examples/core.md)_
    
  • SKILL.md 10.4 KB
    ---
    name: api-database-vercel-kv
    description: Serverless Redis-compatible key-value store via Upstash REST API -- edge-compatible, automatic JSON serialization, TTL-based caching
    ---
    
    # Vercel KV / Upstash Redis Patterns
    
    > **Quick Guide:** Use `@upstash/redis` (the successor to `@vercel/kv`) for serverless, edge-compatible Redis via REST API. Key gotchas: REST adds ~5-15ms latency per call vs TCP Redis, all values are auto-serialized as JSON (objects round-trip transparently but `Date` objects become strings), pipeline/multi execute as single HTTP requests but pipeline is NOT atomic. Use `Redis.fromEnv()` for automatic connection. Always set TTLs -- serverless Redis is billed per command.
    
    ---
    
    <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 `@upstash/redis` for new projects -- `@vercel/kv` was deprecated in December 2024 and all stores were migrated to Upstash Redis)**
    
    **(You MUST set TTLs on all cached data -- serverless Redis is billed per command and has storage limits per plan)**
    
    **(You MUST understand that this is a REST/HTTP client, NOT a TCP Redis client -- each command is an HTTP request with ~5-15ms overhead, so batch with pipelines when possible)**
    
    </critical_requirements>
    
    ---
    
    ## Examples
    
    - [Core Patterns](examples/core.md) -- Client setup, CRUD operations, TTL, hashes, pipelines, transactions, rate limiting, sessions
    
    **Additional resources:**
    
    - [reference.md](reference.md) -- Command quick reference, environment variables, plan limits
    
    ---
    
    **Auto-detection:** Vercel KV, @vercel/kv, @upstash/redis, Upstash Redis, KV_REST_API_URL, KV_REST_API_TOKEN, UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN, Redis.fromEnv, kv.set, kv.get, kv.hset, kv.hget, kv.incr, kv.expire, kv.del, createClient, automaticDeserialization, edge Redis, serverless Redis
    
    **When to use:**
    
    - Caching API responses or database queries in Vercel serverless/edge functions
    - Rate limiting at the edge (sliding window counters)
    - Session storage for serverless applications
    - Feature flags, A/B test assignments, or short-lived counters
    - Any Redis use case on Vercel where TCP connections are unavailable (edge runtime)
    
    **Key patterns covered:**
    
    - Client initialization (`Redis.fromEnv()`, `new Redis()`)
    - Basic CRUD with automatic JSON serialization
    - TTL and expiration strategies
    - Hash operations for structured data
    - Pipelines (batched HTTP) and transactions (atomic MULTI/EXEC)
    - Rate limiting with sorted sets
    - Session storage patterns
    
    **When NOT to use:**
    
    - High-throughput, low-latency Redis workloads (use ioredis with TCP -- REST adds per-request overhead)
    - Pub/Sub subscribers (REST is request-response, not persistent connections)
    - Redis Streams consumers (requires TCP client like ioredis)
    - Large value storage (>1 MB per record on free tier, billed by command count)
    - Primary database (Redis is a cache/ephemeral store, not a source of truth)
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    Upstash Redis (formerly Vercel KV) is a **serverless, REST-based Redis** designed for edge and serverless runtimes where TCP connections are unavailable or impractical. The core trade-off: **HTTP compatibility everywhere, at the cost of per-request latency overhead.**
    
    **Core principles:**
    
    1. **REST-first** -- Every Redis command is an HTTP request. This works everywhere (edge, serverless, browsers) but adds ~5-15ms per call. Batch with pipelines.
    2. **Auto-serialization** -- Objects are JSON-serialized on write and deserialized on read. This is convenient but means `Date` objects, `Map`, `Set`, and functions are not preserved faithfully.
    3. **Ephemeral by design** -- Set TTLs on everything. Serverless Redis is billed per command and has storage caps. Treat it as a cache, not a database.
    4. **Zero connection management** -- No connection pools, no reconnection logic, no `error` event handlers. Each request is stateless HTTP.
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    > Full implementations with good/bad pairs: [examples/core.md](examples/core.md)
    
    ### Pattern 1: Client Initialization
    
    Two approaches: `Redis.fromEnv()` (preferred on Vercel -- reads `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` automatically) or `new Redis({ url, token })` for explicit configuration. Never hardcode credentials.
    
    ```typescript
    import { Redis } from "@upstash/redis";
    const redis = Redis.fromEnv();
    export { redis };
    ```
    
    ---
    
    ### Pattern 2: Automatic JSON Serialization
    
    The SDK auto-serializes objects to JSON on write and deserializes on read. Never call `JSON.stringify` manually -- it causes double-serialization. Use `get<T>()` for typed returns, `satisfies` for type-safe writes. `Date` objects become ISO strings on round-trip -- store timestamps as numbers instead.
    
    ```typescript
    await redis.set("user:123", data satisfies UserProfile, { ex: TTL_SECONDS });
    const user = await redis.get<UserProfile>("user:123"); // UserProfile | null
    ```
    
    ---
    
    ### Pattern 3: TTL and Expiration
    
    Always set TTLs -- serverless Redis is billed per command. Use `{ ex: seconds }` or `{ px: milliseconds }` on `set()`. Use `{ nx: true }` for distributed locks (returns `"OK"` or `null`). Keys without TTLs cause unbounded storage growth.
    
    ```typescript
    await redis.set("cache:key", data, { ex: CACHE_TTL_SECONDS });
    ```
    
    ---
    
    ### Pattern 4: Hash Operations
    
    Hashes enable partial field reads/writes without serializing entire objects. Use `hset` for multi-field writes, `hget`/`hgetall` for reads, `hincrby` for atomic counters. Note: `hset` does not accept TTL directly -- call `expire()` separately. `hgetall` returns `null` for missing keys (not `{}`).
    
    ---
    
    ### Pattern 5: Pipelines and Transactions
    
    **Pipelines** (`redis.pipeline()`) batch commands into a single HTTP request but are NOT atomic. **Transactions** (`redis.multi()`) provide atomic MULTI/EXEC, also as a single HTTP request. Avoid sequential calls when multiple commands can be batched -- each call is a separate HTTP round-trip.
    
    ```typescript
    const pipe = redis.pipeline();
    pipe.set("k1", "v1", { ex: TTL });
    pipe.incr("counter");
    const results = await pipe.exec<[string, number]>();
    ```
    
    **Important:** Upstash REST transactions do NOT support `WATCH` for optimistic locking.
    
    ---
    
    ### Pattern 6: Rate Limiting (Sliding Window)
    
    Sliding window via sorted set scores -- `zadd` with timestamp as score, `zremrangebyscore` to prune expired entries, `zcard` to count, all batched in a pipeline. For production rate limiting, consider `@upstash/ratelimit` which provides built-in algorithms.
    
    ---
    
    ### Pattern 7: Cache-Aside Helper
    
    Generic `cacheAside<T>(key, fetcher, ttl)` pattern: check cache first, fetch on miss, fire-and-forget cache write to avoid blocking responses on cache failures.
    
    </patterns>
    
    ---
    
    <decision_framework>
    
    ## Decision Framework
    
    ### Upstash Redis vs ioredis/node-redis?
    
    ```
    Which Redis client should I use?
    +-- Running in Vercel Edge Runtime? -> @upstash/redis (only option -- no TCP)
    +-- Running in Vercel Serverless Functions? -> @upstash/redis (simpler) or ioredis (if you need TCP features)
    +-- Need Pub/Sub subscribers? -> ioredis (REST cannot maintain subscriptions)
    +-- Need Redis Streams consumers? -> ioredis (requires persistent TCP connection)
    +-- Need lowest possible latency (<1ms)? -> ioredis with TCP (REST adds HTTP overhead)
    +-- Simple caching/sessions/counters? -> @upstash/redis (zero connection management)
    ```
    
    ### Pipeline vs Transaction vs Sequential?
    
    ```
    How should I batch commands?
    +-- Need atomicity (all-or-nothing)? -> redis.multi() (transaction)
    +-- Just reducing HTTP round-trips? -> redis.pipeline() (non-atomic batch)
    +-- Single independent command? -> Direct call (redis.set, redis.get, etc.)
    ```
    
    </decision_framework>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority Issues:**
    
    - Using `@vercel/kv` in new projects -- deprecated December 2024, use `@upstash/redis` instead
    - Missing TTLs on cached keys -- causes unbounded storage growth and unexpected billing
    - Manual `JSON.stringify`/`JSON.parse` with Upstash Redis -- causes double-serialization because the SDK auto-serializes all values
    - Assuming pipeline commands are atomic -- pipelines batch for HTTP efficiency but do NOT guarantee atomicity (use `multi()` for atomic execution)
    
    **Medium Priority Issues:**
    
    - Making sequential Redis calls where a pipeline would work -- each call is a separate HTTP round-trip (~5-15ms each)
    - Storing values >1 MB -- REST requests have size limits per plan (100 MB max on free/pay-as-you-go, but large values degrade performance)
    - Using Upstash Redis as a primary database -- it's a cache/ephemeral store, always have a source of truth elsewhere
    
    **Common Mistakes:**
    
    - Expecting `hgetall` to return an empty object `{}` for missing keys -- Upstash returns `null` (unlike ioredis which returns `{}`)
    - Forgetting that `get()` returns `null` (not `undefined`) for missing keys
    - Passing `Date` objects and expecting them to survive round-trip -- they serialize to ISO strings and come back as strings, not `Date` instances
    
    **Gotchas & Edge Cases:**
    
    - `automaticDeserialization: false` breaks many TypeScript types -- only disable if you need raw string responses and are prepared to handle typing manually
    - `set` with `ex` option resets TTL on overwrite (standard Redis behavior) -- if you `set` a key that already has a TTL, the new `ex` value replaces it
    - REST latency is per-request, not per-command -- a pipeline with 10 commands has the same HTTP overhead as a single command (one round-trip)
    - Free tier is limited to 500K commands/month and 256 MB storage -- monitor usage in production
    - `nx` (set-if-not-exists) returns `null` on failure, `"OK"` on success -- check the return value explicitly
    
    </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 `@upstash/redis` for new projects -- `@vercel/kv` was deprecated in December 2024 and all stores were migrated to Upstash Redis)**
    
    **(You MUST set TTLs on all cached data -- serverless Redis is billed per command and has storage limits per plan)**
    
    **(You MUST understand that this is a REST/HTTP client, NOT a TCP Redis client -- each command is an HTTP request with ~5-15ms overhead, so batch with pipelines when possible)**
    
    **Failure to follow these rules will cause deprecated package usage, unbounded storage costs, and unnecessary latency in serverless functions.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related