Vercel KV was Vercel's serverless Redis product, and it no longer exists under that name. Vercel's documentation now says Vercel KV is no longer available and that existing KV stores were moved automatically to Upstash Redis in December 2024. New projects get Redis by installing an integration from the Vercel Marketplace. The npm package @vercel/kv is marked deprecated with the same message. Its last release, 3.0.0, is a thin wrapper that depends on @upstash/redis.
Readers searching for Vercel KV usually have old code that imports @vercel/kv, or want the same capability today. This page covers both: the architecture (Redis commands over HTTPS to Upstash), connecting and migrating, the core patterns, pipelines, global replicas, a worked cost example, failure modes and a checklist. Product details were checked against the Vercel and Upstash documentation on 2026-10-04; confirm prices and limits again before you depend on them.
What Vercel KV was, and what it is now
Vercel KV was a Redis-compatible store created from the Vercel dashboard, provided under the hood by Upstash, with a client library that wrapped Upstash's HTTP client. When Vercel moved its storage products to Marketplace integrations, KV stores became ordinary Upstash Redis databases managed through the Upstash integration. The data and credentials stayed the same; the billing relationship and the settings page moved.
Two consequences follow. Any guide that tells you to click Create KV Database in Vercel Storage is out of date: the path now is Marketplace, then a Redis provider, then connecting it to a project, which injects credentials as environment variables. And the limits that matter are Upstash's: command pricing, request and record sizes, replication and client SDKs. The Marketplace lists other Redis providers too. A provider with only a TCP endpoint works, but brings back the connection problems described next.
The architecture: Redis commands over HTTPS
Classic Redis clients hold a long-lived TCP connection, which suits a server process that lives for days. Serverless functions scale from zero to hundreds of instances, each opening its own connection, and may be frozen between invocations with sockets half-open. Server connection limits become the scaling ceiling, and cold starts pay a TCP and TLS handshake first.
Upstash's answer is a REST interface. Each command, or batch of commands, is an HTTPS request to a database-specific URL with a bearer token. The @upstash/redis client turns calls such as redis.get(key) into those requests and parses the JSON responses. Nothing persists between calls, so the client works in the Edge runtime, and a burst of instances cannot exhaust a connection pool.
The price is that every unbatched command is a full HTTPS round trip, so latency depends on the distance between the function's region and the database's. The most important deployment decision is to put the database's primary region next to your functions.
Connecting from a Vercel function
The Upstash TypeScript client has two constructors. Redis.fromEnv() reads UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN. The explicit constructor takes a URL and token from wherever you like. Stores migrated from Vercel KV kept the KV_REST_API_URL and KV_REST_API_TOKEN names that @vercel/kv reads. Integration-created stores may use different names. Open the project's Environment Variables page, see which names are actually there, and pass them explicitly. A wrong guess fails at runtime, not at build time.
// lib/redis.ts - one module-level client, reused across invocations of a warm instance
import { Redis } from "@upstash/redis";
const url = process.env.KV_REST_API_URL; // use the names your project actually has
const token = process.env.KV_REST_API_TOKEN;
if (!url || !token) {
throw new Error("Redis credentials missing: check the project's environment variables");
}
export const redis = new Redis({ url, token });Fail loudly when credentials are missing, because a silent fallback to no cache turns a configuration error into a latency incident. And check which environments the variables are attached to. Vercel scopes variables to Production, Preview and Development, so a store shared across all three lets preview deployments overwrite production keys. Use a separate database for previews, or at least a per-environment key prefix.
Migrating code that imports @vercel/kv
Because @vercel/kv wrapped @upstash/redis, migration is mostly an import change. The kv default export read the KV_REST_API_* variables. Replace it with an Upstash client built from the same variables, keep the variable name kv if you want a minimal diff, and remove the deprecated package.
// before
import { kv } from "@vercel/kv";
await kv.set("greeting", "hello", { ex: 60 });
// after
import { Redis } from "@upstash/redis";
export const kv = new Redis({
url: process.env.KV_REST_API_URL!,
token: process.env.KV_REST_API_TOKEN!,
});
await kv.set("greeting", "hello", { ex: 60 });Run your tests against a non-production database and pin the @upstash/redis version you tested. Watch serialization: the client serializes objects to JSON on write and parses JSON on read by default, so a raw string written by another client, such as "123", can come back as a number.
Caching, sessions and rate limits
A key-value store earns its place on Vercel in three jobs: caching expensive reads, holding short-lived state between stateless invocations, and counting things across instances. Each has a standard shape.
Cache-aside with a TTL. Read the key. On a miss, load from the source of truth, write the value with an expiry and return it. Version the key prefix so a schema change invalidates old entries without a scan. Add a little random jitter to the TTL so keys written together do not expire together.
type Product = { id: string; name: string; priceCents: number };
export async function getProduct(id: string): Promise<Product | null> {
const key = `product:v3:${id}`;
const hit = await redis.get<Product>(key);
if (hit) return hit;
const row = await db.product.findUnique({ where: { id } }); // source of truth
if (row) {
const ttl = 300 + Math.floor(Math.random() * 60); // 5-6 minutes, jittered
await redis.set(key, row, { ex: ttl });
}
return row;
}Sessions. Store a session as a JSON value under a random ID held in an HTTP-only cookie, with an expiry refreshed on activity. Never make the store the only copy of anything you cannot recreate.
Rate limiting. @upstash/ratelimit implements fixed-window, sliding-window and token-bucket limiters on the same client:
import { Ratelimit } from "@upstash/ratelimit";
import { redis } from "@/lib/redis";
const limiter = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(20, "60 s"), // 20 requests per rolling minute
prefix: "rl:summarize",
});
export async function POST(req: Request) {
const id = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "anonymous";
const { success, remaining } = await limiter.limit(id);
if (!success) return new Response("Too many requests", { status: 429 });
// ... call the expensive model, include remaining in a header if useful
return Response.json({ ok: true, remaining });
}Prefer a user ID to an IP: many users share an IP behind NAT. Decide in advance what happens if Redis is unreachable. Failing open keeps the product up but removes protection; failing closed turns a store outage into an API outage.
Pipelines, transactions and auto-pipelining
Because every call is an HTTPS request, the way to cut latency is to send fewer requests. The client offers three tools, and they differ in atomicity.
A pipeline batches commands into one HTTP request. Upstash documents that pipeline execution is not atomic: other clients' commands can interleave between yours. A transaction created with multi() also travels as one request but executes atomically. Auto-pipelining lets the client batch commands issued in the same tick automatically. It works best when you start several commands and await them together with Promise.all, rather than awaiting each one in turn. It can be turned off with enableAutoPipelining: false.
// Count a page view and update a trending set in one round trip.
const tx = redis.multi();
tx.incr(`views:${slug}`);
tx.expire(`views:${slug}`, 86_400);
tx.zincrby("trending:today", 1, slug);
const [views] = await tx.exec<[number, number, number]>();Use a pipeline when commands are independent, and multi() when a reader must never see a half-applied update. Neither gives compare-and-set: if a decision depends on a value you just read, it has to run server-side.
Global databases and consistency
Upstash can run a Global Database: one primary region that takes writes, plus read regions near users. The documentation is explicit about the trade. Replication is asynchronous, the database is eventually consistent, and a read may return a stale value while a write propagates. Each replicated write is billed, so one primary and two read regions bill three commands per write.
Global replicas suit read-heavy data that tolerates seconds of staleness: feature flags, configuration, cached fragments. They are a poor fit when a user writes then immediately reads, such as a session update followed by a redirect, or a limiter that must count accurately. Route those to the primary, or keep them in a single-region database.
Worked example: 2 million page views a day
Take a product site on Vercel serving 2 million page views a day. Each view does one cache-aside GET for product data, with a 90 percent hit rate. Misses add one SET. A summarize endpoint gets 50,000 calls a day behind a sliding-window limiter. The figures below use the Upstash pay-as-you-go limits and price published on 2026-10-04: 10,000 commands per second, a 10 MB maximum request, a 100 MB maximum record, 100 GB maximum data, and $0.2 per 100,000 commands.
| Item | Per day | Notes |
|---|---|---|
| Cache GETs | 2,000,000 | one per page view |
| Cache SETs on miss | 200,000 | 10 percent miss rate |
| Rate-limit commands | measure it | the limiter's command count per call depends on the algorithm; read it from the Upstash console after a day of traffic |
| Total without limiter | 2,200,000 | about 66 million commands a month |
| Monthly command cost | about $132 | 66,000,000 / 100,000 x $0.2, before storage and bandwidth |
The average is about 25 commands per second; even a tenfold peak is far below 10,000 per second, so cost is the constraint, not throughput. A better hit rate barely helps because GETs dominate. Fewer commands per view does: serve the page statically so most views never reach a function, or batch lookups into one MGET. Two read regions would add 400,000 billed commands a day for replicated SETs. Fixed plans price by data size, so compare both models on Upstash's current pricing page.
Failure modes
Most incidents with Redis on Vercel come from a short list of causes.
- Region mismatch. Functions in one region and the database in another add a cross-region round trip to every command. Symptom: p50 latency far above what the work needs. Fix: co-locate them, and batch.
- Serial awaits. Ten sequential
await redis.getcalls are ten round trips. Usemget, a pipeline orPromise.allwith auto-pipelining. - Missing TTLs. Cache keys written without
exaccumulate until the data limit. Check the database's eviction setting. If eviction is off, writes start failing when the database is full. - Shared store across environments. Preview deployments writing production keys. Use separate databases or per-environment prefixes.
- Oversized values. Requests are capped at 10 MB on pay-as-you-go. A cached API response that keeps growing eventually fails to write. Store references or compress.
- Stale reads from replicas. A read region returns the old value after a write. Route read-after-write paths to the primary.
- No timeout. A slow store makes every request slow. Set a client-side timeout and a fallback.
- Leaked tokens. The REST token grants full access. Keep it out of client bundles and rotate it if it leaks.
Trade-offs
| Choice | Gains | Costs |
|---|---|---|
| HTTP client (Upstash REST) | No connection pool, works in Edge runtime, scales with function count | One HTTPS round trip per unbatched call; per-command billing |
| TCP Redis provider | Lower per-command latency from long-lived servers; full protocol | Connection churn and limits from serverless functions; no Edge runtime |
| Single-region database | Read-your-writes, cheaper writes | Far users pay distance on reads |
| Global Database | Low-latency reads near users | Eventual consistency; each write billed once per region |
| Pipeline | One round trip | Not atomic |
| multi() transaction | One round trip, atomic | Still no conditional logic on read values |
If the data must be durable, Postgres is the source of truth and Redis only its cache. For the underlying engine, see Redis in depth and ElastiCache in depth. For the platform side, see Vercel Edge Functions and Vercel Postgres (Neon). For the limiter algorithms themselves, see rate limiter architecture, and for when cached data goes stale, cache invalidation strategies.
What to do next
- Search your code for
@vercel/kv. Replace it with an explicit@upstash/redisclient built from the environment variable names your project actually has, then uninstall the deprecated package. - Open the database in the Upstash console. Confirm its primary region matches your Vercel function region, and move one or the other if they differ.
- Check which Vercel environments the credentials are attached to. Give Preview its own database, or a key prefix at minimum.
- Audit every write for a TTL, version the cache key prefixes, and check the database's eviction setting.
- Find request paths that await Redis serially and convert them to
mget,multi()or batchedPromise.allcalls. - Put a rate limiter in front of each expensive or abusable route, keyed by user ID where possible, and write down whether it fails open or closed.
- After a week, read the command count from the console, recompute the monthly cost the way the worked example does, and compare pay-as-you-go against fixed plans.
- If you add read regions, list every read-after-write path and pin it to the primary first.