Learn-skills.dev api-database-vercel-kv
Serverless Redis-compatible key-value store via Upstash REST API -- edge-compatible, automatic JSON serialization, TTL-based caching
git clone https://github.com/NeverSight/learn-skills.dev
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/agents-inc/skills/api-database-vercel-kv" ~/.claude/skills/neversight-learn-skills-dev-api-database-vercel-kv && rm -rf "$T"
data/skills-md/agents-inc/skills/api-database-vercel-kv/SKILL.mdVercel KV / Upstash Redis Patterns
Quick Guide: Use
(the successor to@upstash/redis) 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@vercel/kvobjects become strings), pipeline/multi execute as single HTTP requests but pipeline is NOT atomic. UseDatefor automatic connection. Always set TTLs -- serverless Redis is billed per command.Redis.fromEnv()
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
, named constants)import type
(You MUST use
for new projects -- @upstash/redis
was deprecated in December 2024 and all stores were migrated to Upstash Redis)@vercel/kv
(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)
<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:
- REST-first -- Every Redis command is an HTTP request. This works everywhere (edge, serverless, browsers) but adds ~5-15ms per call. Batch with pipelines.
- Auto-serialization -- Objects are JSON-serialized on write and deserialized on read. This is convenient but means
objects,Date
,Map
, and functions are not preserved faithfully.Set - 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.
- Zero connection management -- No connection pools, no reconnection logic, no
event handlers. Each request is stateless HTTP.error
<patterns>
Core Patterns
Full implementations with good/bad pairs: 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.
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.
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.
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.
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
in new projects -- deprecated December 2024, use@vercel/kv
instead@upstash/redis - Missing TTLs on cached keys -- causes unbounded storage growth and unexpected billing
- Manual
/JSON.stringify
with Upstash Redis -- causes double-serialization because the SDK auto-serializes all valuesJSON.parse - Assuming pipeline commands are atomic -- pipelines batch for HTTP efficiency but do NOT guarantee atomicity (use
for atomic execution)multi()
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
to return an empty objecthgetall
for missing keys -- Upstash returns{}
(unlike ioredis which returnsnull
){} - Forgetting that
returnsget()
(notnull
) for missing keysundefined - Passing
objects and expecting them to survive round-trip -- they serialize to ISO strings and come back as strings, notDate
instancesDate
Gotchas & Edge Cases:
breaks many TypeScript types -- only disable if you need raw string responses and are prepared to handle typing manuallyautomaticDeserialization: false
withset
option resets TTL on overwrite (standard Redis behavior) -- if youex
a key that already has a TTL, the newset
value replaces itex- 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
(set-if-not-exists) returnsnx
on failure,null
on success -- check the return value explicitly"OK"
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
, named constants)import type
(You MUST use
for new projects -- @upstash/redis
was deprecated in December 2024 and all stores were migrated to Upstash Redis)@vercel/kv
(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>