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.

A Vercel function talks to Upstash Redis over HTTPS, not a Redis TCP socketBrowserpage or API callVercel FunctionNode or Edge runtime@upstash/redis clientUpstash Redisprimary regionREST endpoint + tokenRead region Aasync replicaRead region Basync replicaHTTPSHTTPS + BearerProject env varsURL + tokenread at runtimeMarketplaceintegrationinjectsprovisionsEach command (or pipeline) is one HTTP request: no connection pool to exhaust, but every call pays a network round trip.Writes go to the primary; read regions are eventually consistent and every replicated write is billed again.
Request path for Redis on Vercel through the Upstash integration. Credentials arrive as project environment variables; each command or pipeline is one authenticated HTTPS request; optional read regions replicate asynchronously.

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.

ItemPer dayNotes
Cache GETs2,000,000one per page view
Cache SETs on miss200,00010 percent miss rate
Rate-limit commandsmeasure itthe limiter's command count per call depends on the algorithm; read it from the Upstash console after a day of traffic
Total without limiter2,200,000about 66 million commands a month
Monthly command costabout $13266,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.get calls are ten round trips. Use mget, a pipeline or Promise.all with auto-pipelining.
  • Missing TTLs. Cache keys written without ex accumulate 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

ChoiceGainsCosts
HTTP client (Upstash REST)No connection pool, works in Edge runtime, scales with function countOne HTTPS round trip per unbatched call; per-command billing
TCP Redis providerLower per-command latency from long-lived servers; full protocolConnection churn and limits from serverless functions; no Edge runtime
Single-region databaseRead-your-writes, cheaper writesFar users pay distance on reads
Global DatabaseLow-latency reads near usersEventual consistency; each write billed once per region
PipelineOne round tripNot atomic
multi() transactionOne round trip, atomicStill 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

  1. Search your code for @vercel/kv. Replace it with an explicit @upstash/redis client built from the environment variable names your project actually has, then uninstall the deprecated package.
  2. 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.
  3. Check which Vercel environments the credentials are attached to. Give Preview its own database, or a key prefix at minimum.
  4. Audit every write for a TTL, version the cache key prefixes, and check the database's eviction setting.
  5. Find request paths that await Redis serially and convert them to mget, multi() or batched Promise.all calls.
  6. 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.
  7. 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.
  8. If you add read regions, list every read-after-write path and pin it to the primary first.
Key takeaway: Vercel KV is gone as a product: its stores became Upstash Redis databases in December 2024, and @vercel/kv is a deprecated wrapper around @upstash/redis. What remains is Redis over HTTPS. There is no connection pool to exhaust, but every unbatched command is a round trip and a billed command. Co-locate the database with your functions, batch with mget, pipelines or multi, give every cache key a TTL and a versioned prefix, keep preview and production apart, and use read regions only for data that tolerates staleness.