api-database-vercel-kv
Serverless Redis-compatible key-value store via Upstash REST API -- edge-compatible, automatic JSON serialization, TTL-based caching
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-database-vercel-kv/skills/api-database-vercel-kv
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
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 butDateobjects become strings), pipeline/multi execute as single HTTP requests but pipeline is NOT atomic. UseRedis.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/kvin new projects -- deprecated December 2024, use@upstash/redisinstead - Missing TTLs on cached keys -- causes unbounded storage growth and unexpected billing
- Manual
JSON.stringify/JSON.parsewith 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
hgetallto return an empty object{}for missing keys -- Upstash returnsnull(unlike ioredis which returns{}) - Forgetting that
get()returnsnull(notundefined) for missing keys - Passing
Dateobjects and expecting them to survive round-trip -- they serialize to ISO strings and come back as strings, notDateinstances
Gotchas & Edge Cases:
automaticDeserialization: falsebreaks many TypeScript types -- only disable if you need raw string responses and are prepared to handle typing manuallysetwithexoption resets TTL on overwrite (standard Redis behavior) -- if youseta key that already has a TTL, the newexvalue 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) returnsnullon 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.
Reviews (0)
No reviews yet.
No comments yet.