Serverless Redis-compatible key-value store via Upstash REST API -- edge-compatible, automatic JSON serialization, TTL-based caching
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>
<philosophy> </philosophy> <patterns> </patterns>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>
Additional resources:
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:
Key patterns covered:
Redis.fromEnv(), new Redis())When NOT to use:
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:
Date objects, Map, Set, and functions are not preserved faithfully.error event handlers. Each request is stateless HTTP.Full implementations with good/bad pairs: examples/core.md
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 };
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
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 });
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 {}).
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.
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.
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.
<decision_framework>
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)
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>
High Priority Issues:
@vercel/kv in new projects -- deprecated December 2024, use @upstash/redis insteadJSON.stringify/JSON.parse with Upstash Redis -- causes double-serialization because the SDK auto-serializes all valuesmulti() for atomic execution)Medium Priority Issues:
Common Mistakes:
hgetall to return an empty object {} for missing keys -- Upstash returns null (unlike ioredis which returns {})get() returns null (not undefined) for missing keysDate objects and expecting them to survive round-trip -- they serialize to ISO strings and come back as strings, not Date instancesGotchas & Edge Cases:
automaticDeserialization: false breaks many TypeScript types -- only disable if you need raw string responses and are prepared to handle typing manuallyset 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 itnx (set-if-not-exists) returns null on failure, "OK" on success -- check the return value explicitly</red_flags>
<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>