vercel-kv
|
Works with
Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
name: vercel-kv
description: |
license: MIT
---
# Vercel KV
**Last Updated**: 2026-01-21
**Version**: @vercel/kv@3.0.0 (Redis-compatible, powered by Upstash)
---
## Quick Start
```bash
# Create KV: Vercel Dashboard → Storage → KV
vercel env pull .env.local # Creates KV_REST_API_URL and KV_REST_API_TOKEN
npm install @vercel/kv
```
**Basic Usage**:
```typescript
import { kv } from '@vercel/kv';
// Set with TTL (expires in 1 hour)
await kv.setex('session:abc', 3600, { userId: 123 });
// Get
const session = await kv.get('session:abc');
// Increment counter (atomic)
const views = await kv.incr('views:post:123');
```
**CRITICAL**: Always use namespaced keys (`user:123` not `123`) and set TTL for temporary data.
---
## Common Patterns
**Caching** (cache-aside):
```typescript
const cached = await kv.get(`post:${slug}`);
if (cached) return cached;
const post = await db.query.posts.findFirst({ where: eq(posts.slug, slug) });
await kv.setex(`post:${slug}`, 3600, post); // Cache 1 hour
return post;
```
**Rate Limiting**:
```typescript
async function checkRateLimit(ip: string): Promise<boolean> {
const key = `ratelimit:${ip}`;
const current = await kv.incr(key);
if (current === 1) await kv.expire(key, 60); // 60s window
return current <= 10; // 10 requests per window
}
```
**Session Management**:
```typescript
const sessionId = crypto.randomUUID();
await kv.setex(`session:${sessionId}`, 7 * 24 * 3600, { userId });
```
**Pipeline** (batch operations):
```typescript
const pipeline = kv.pipeline();
pipeline.set('user:1', data);
pipeline.incr('counter');
const results = await pipeline.exec(); // Single round-trip
```
**Key Naming**: Use namespaces like `user:123`, `post:abc:views`, `ratelimit:ip:endpoint`
---
## Critical Rules
**Always**:
- ✅ Set TTL for temporary data (`setex` not `set`)
- ✅ Use namespaced keys (`user:123` not `123`)
- ✅ Handle null returns (non-existent keys)
- ✅ Use pipeline for batch operations
**Never**:
- ❌ Forget to set TTL (memory leak)
- ❌ Store large values >1MB (use Vercel Blob)
- ❌ Use KV as primary database (it's a cache)
- ❌ Store non-JSON-serializable data (functions, BigInt, circular refs)
---
## Known Issues Prevention
This skill prevents **15 documented issues**:
### Issue #1: Missing Environment Variables
**Error**: `Error: KV_REST_API_URL is not defined` or `KV_REST_API_TOKEN is not defined`
**Source**: https://vercel.com/docs/storage/vercel-kv/quickstart | [GitHub Issue #759](https://github.com/vercel/storage/issues/759)
**Why It Happens**: Environment variables not set locally or in deployment. In monorepos (Turborepo/pnpm workspaces), abstracting `@vercel/kv` into a shared package can cause Vercel builds to fail even though local builds work.
**Prevention**: Run `vercel env pull .env.local` and ensure `.env.local` is in `.gitignore`. For monorepos, either (1) create client in consuming app not shared package, (2) use Vercel Environment Variables UI to set at project level, or (3) add env vars to `turbo.json` pipeline config: `{ "pipeline": { "build": { "env": ["KV_REST_API_URL", "KV_REST_API_TOKEN"] } } }`.
### Issue #2: JSON Serialization Error
**Error**: `TypeError: Do not know how to serialize a BigInt` or circular reference errors. Also, `hset()` coerces numeric strings to numbers.
**Source**: https://github.com/vercel/storage/issues/89 | [GitHub Issue #727](https://github.com/vercel/storage/issues/727)
**Why It Happens**: Trying to store non-JSON-serializable data (functions, BigInt, circular refs). Additionally, when using `hset()` to store string values that look numeric (e.g., `'123456'`), `hgetall()` returns them as numbers, breaking type consistency.
**Prevention**: Only store plain objects, arrays, strings, numbers, booleans, null. Convert BigInt to string. For hash fields with numeric strings, either (1) use non-numeric prefix like `'code_123456'`, (2) store as JSON string and parse after retrieval, or (3) validate and recast types: `String(value.field)` after `hgetall()`.
### Issue #3: Key Naming Collisions
**Error**: Unexpected data returned, data overwritten by different feature
**Source**: Production debugging, best practices
**Why It Happens**: Using generic key names like `cache`, `data`, `temp` across different features
**Prevention**: Always use namespaced keys: `feature:id:type` pattern.
### Issue #4: TTL Not Set
**Error**: Memory usage grows indefinitely, old data never expires
**Source**: Vercel KV best practices
**Why It Happens**: Using `set()` without `setex()` for temporary data
**Prevention**: Use `setex(key, ttl, value)` for all temporary data. Set appropriate TTL (seconds).
### Issue #5: Rate Limit Exceeded (Free Tier)
**Error**: `Error: Rate limit exceeded` or commands failing
**Source**: https://vercel.com/docs/storage/vercel-kv/limits
**Why It Happens**: Exceeding 30,000 commands/month on free tier
**Prevention**: Monitor usage in Vercel dashboard, upgrade plan if needed, use caching to reduce KV calls.
### Issue #6: Storing Large Values
**Error**: `Error: Value too large` or performance degradation
**Source**: https://vercel.com/docs/storage/vercel-kv/limits
**Why It Happens**: Trying to store values >1MB in KV
**Prevention**: Use Vercel Blob for files/images. Keep KV values small (<100KB recommended).
### Issue #7: Type Mismatch on Get
**Error**: TypeScript errors, runtime type errors. Generic `kv.get<T>()` sometimes returns `null` even when data exists.
**Source**: Common TypeScript issue | [GitHub Issue #510](https://github.com/vercel/storage/issues/510)
**Why It Happens**: `kv.get()` returns `unknown` type, need to cast or validate. Additionally, there's a type inference bug where using generics like `kv.get<T>()` can cause the function to return `null` even when CLI shows data exists, due to serialization/deserialization issues.
**Prevention**: Don't use generics with `get()`. Instead, retrieve without type parameter and cast after retrieval: `const rawData = await kv.get('key'); const data = rawData as MyType | null;`. Validate with Zod or type guards before using.
### Issue #8: Pipeline Errors Not Handled
**Error**: Silent failures, partial execution
**Source**: https://github.com/vercel/storage/issues/120
**Why It Happens**: Pipeline execution can have individual command failures
**Prevention**: Check results array from `pipeline.exec()` and handle errors.
### Issue #9: Scan Operation Inefficiency
**Error**: Slow queries, timeout errors. In v3.0.0+, cursor type changed from `number` to `string`.
**Source**: Redis best practices | [Release Notes v3.0.0](https://github.com/vercel/storage/releases/tag/@vercel/kv@3.0.0)
**Why It Happens**: Using `scan()` with large datasets or wrong cursor handling. Version 3.0.0 introduced a breaking change where scan cursor is now `string` instead of `number`.
**Prevention**: Limit `count` parameter, iterate properly with cursor, avoid full scans in production. In v3.0.0+, use `let cursor: string = "0"` and compare with `cursor !== "0"` (not `!== 0`).
### Issue #10: Missing TTL Refresh
**Error**: Session expires too early, cache invalidates prematurely
**Source**: Production debugging
**Why It Happens**: Not refreshing TTL on access (sliding expiration)
**Prevention**: Use `expire(key, newTTL)` on access to implement sliding windows.
### Issue #11: scanIterator() Infinite Loop (v2.0.0+)
**Error**: `for await` loop never terminates when using `kv.scanIterator()`
**Source**: [GitHub Issue #706](https://github.com/vercel/storage/issues/706)
**Why It Happens**: Bug in v2.0.0+ where iterator doesn't properly signal completion. The iterator processes keys correctly but never exits, preventing the function from returning. Also affects `sscanIterator()`.
**Prevention**: Use manual `scan()` with cursor instead of `scanIterator()`.
```typescript
// Don't use scanIterator() - it hangs in v2.0.0+
for await (const key of kv.scanIterator()) {
// This loop never terminates
}
// Use manual scan with cursor instead
let cursor: string = "0"; // v3.x uses string
do {
const [newCursor, keys] = await kv.scan(cursor);
cursor = newCursor;
for (const key of keys) {
const value = await kv.get(key);
// process key/value
}
} while (cursor !== "0");
```
### Issue #12: zrange() with rev: true Returns Empty Array
**Error**: `kv.zrange(key, 0, -1, { rev: true })` returns empty array even though data exists
**Source**: [GitHub Issue #742](https://github.com/vercel/storage/issues/742)
**Why It Happens**: SDK bug in reverse flag handling for certain key patterns. CLI always returns correct values. Removing the `rev` flag returns data correctly.
**Prevention**: Omit `rev` flag and reverse in-memory, or use `zrevrange()` instead.
```typescript
// This sometimes returns empty array (BUG)
const chats = await kv.zrange(`user:chat:${userId}`, 0, -1, { rev: true });
// [] - but CLI shows 12 items
// Workaround 1: Omit rev flag and reverse in-memory
const chats = await kv.zrange(`user:chat:${userId}`, 0, -1);
const reversedChats = chats.reverse();
// Workaround 2: Use zrevrange instead
const chats = await kv.zrevrange(`user:chat:${userId}`, 0, -1);
```
### Issue #13: Next.js Dev Server Returns Null on First Call
**Error**: `kv.get()` returns `null` on first call after starting dev server, correct data on subsequent calls
**Source**: [GitHub Issue #781](https://github.com/vercel/storage/issues/781)
**Why It Happens**: Next.js static rendering caches the first response. Happens on each compilation restart in Next.js dev server.
**Prevention**: Force dynamic rendering with `unstable_noStore()`, use `cache: 'no-store'` in fetch calls, or add retry logic.
```typescript
import { unstable_noStore as noStore } from 'next/cache';
export async function getData() {
noStore(); // Force dynamic rendering
const data = await kv.get('mykey');
return data; // Now returns correct value on first call
}
// Or add retry logic
async function getWithRetry(key: string, retries = 2) {
let data = await kv.get(key);
let attempt = 0;
while (!data && attempt < retries) {
await new Promise(r => setTimeout(r, 100));
data = await kv.get(key);
attempt++;
}
return data;
}
```
### Issue #14: Vite "process is not defined" Error
**Error**: `Uncaught ReferenceError: process is not defined` when importing `@vercel/kv` in Vite
**Source**: [GitHub Issue #743](https://github.com/vercel/storage/issues/743)
**Why It Happens**: Vite doesn't polyfill Node.js `process.env` by default. `@vercel/kv` expects `process.env` to exist.
**Prevention**: Use Vite's `define` to polyfill, use `createClient` with `import.meta.env`, or install `vite-plugin-node-polyfills`.
```typescript
// Option 1: Vite config with define
// vite.config.ts
import { defineConfig, loadEnv } from 'vite';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
return {
define: {
'process.env.KV_REST_API_URL': JSON.stringify(env.KV_REST_API_URL),
'process.env.KV_REST_API_TOKEN': JSON.stringify(env.KV_REST_API_TOKEN),
},
};
});
// Option 2: Use createClient with import.meta.env
import { createClient } from '@vercel/kv';
const kv = createClient({
url: import.meta.env.VITE_KV_REST_API_URL,
token: import.meta.env.VITE_KV_REST_API_TOKEN,
});
```
### Issue #15: Next.js Server Actions Return Stale Data
**Error**: `kv.get()` in Server Actions returns stale/cached data even after KV values are updated
**Source**: [GitHub Issue #510](https://github.com/vercel/storage/issues/510)
**Why It Happens**: Next.js static rendering caches Server Action responses. Console output appears yellow (cache hit indicator).
**Prevention**: Use `unstable_noStore()` to force dynamic rendering, or use route handlers instead of Server Actions.
```typescript
// In Server Actions
'use server'
import { unstable_noStore as noStore } from 'next/cache';
export async function logChat(text: string) {
noStore(); // Force dynamic rendering
let n_usage = await kv.get('n_usage');
// Now returns fresh value, not cached
}
// Or use route handlers (automatically dynamic)
// app/api/chat/route.ts
export async function GET() {
let n_usage = await kv.get('n_usage'); // Fresh data
return Response.json({ n_usage });
}
```
---
## Version Migration Guide
### v3.0.0 Breaking Changes
**Cursor Type Changed** ([Release Notes](https://github.com/vercel/storage/releases/tag/@vercel/kv@3.0.0)):
- Scan cursor now returns `string` instead of `number`
- Update comparisons from `cursor !== 0` to `cursor !== "0"`
```typescript
// v2.x
let cursor: number = 0;
do {
const [newCursor, keys] = await kv.scan(cursor);
cursor = newCursor;
} while (cursor !== 0);
// v3.x
let cursor: string = "0";
do {
const [newCursor, keys] = await kv.scan(cursor);
cursor = newCursor;
} while (cursor !== "0");
```
### v2.0.0 Breaking Changes
**Auto-Pipelining Enabled by Default** ([Release Notes](https://github.com/vercel/storage/releases/tag/@vercel/kv@2.0.0)):
- Commands now automatically batched for performance
- May cause timing issues in edge cases
- Disable with `enableAutoPipelining: false` if needed
```typescript
// If auto-pipelining causes issues, disable it:
import { createClient } from '@vercel/kv';
const kv = createClient({
url: process.env.KV_REST_API_URL,
token: process.env.KV_REST_API_TOKEN,
enableAutoPipelining: false // Disable auto-pipelining
});
```
**scanIterator() Bug Introduced**: v2.0.0+ has infinite loop bug with `scanIterator()`. See Issue #11 for workaround.
---
## Known Limitations
### Redis Streams Not Supported
**Source**: [Vercel KV Redis Compatibility](https://vercel.com/docs/storage/vercel-kv/redis-compatibility) | [GitHub Issue #278](https://github.com/vercel/storage/issues/278)
`@vercel/kv` does not support Redis Streams (XREAD, XADD, XGROUP, etc.) even though underlying Upstash Redis supports them. The package lacks stream methods.
**Workaround**: Use `@upstash/redis` directly for streams:
```typescript
// @vercel/kv doesn't have stream methods
// await kv.xAdd(...) // TypeError: kv.xAdd is not a function
// Use Upstash Redis client directly
import { Redis } from '@upstash/redis';
const redis = new Redis({
url: process.env.KV_REST_API_URL!,
token: process.env.KV_REST_API_TOKEN!,
});
await redis.xadd('stream:events', '*', { event: 'user.login' });
const messages = await redis.xread('stream:events', '0');
```
### Hash Operations Return Objects Not Strings
**Source**: [GitHub Issue #674](https://github.com/vercel/storage/issues/674)
`kv.hgetall()` returns a JavaScript object, not a JSON string. This confuses developers expecting string serialization.
```typescript
await kv.hset('foobar', { '1834': 'https://example.com' });
const data = await kv.hgetall('foobar');
console.log(typeof data); // "object" not "string"
// It's already an object - use directly
console.log(data['1834']); // 'https://example.com'
// If you need JSON string
const jsonString = JSON.stringify(data);
```
---
## Advanced Patterns
**Distributed Lock** (prevents race conditions):
```typescript
const lockKey = `lock:${resource}`;
const lockValue = crypto.randomUUID();
const acquired = await kv.setnx(lockKey, lockValue);
if (acquired) {
await kv.expire(lockKey, 10); // TTL prevents deadlock
try {
await processOrders();
} finally {
const current = await kv.get(lockKey);
if (current === lockValue) await kv.del(lockKey);
}
}
```
**Leaderboard** (sorted sets):
```typescript
await kv.zadd('leaderboard', { score, member: userId.toString() });
// Note: zrange with { rev: true } has a known bug (Issue #12)
// Use zrevrange instead for reliability
const top = await kv.zrevrange('leaderboard', 0, 9, { withScores: true });
const rank = await kv.zrevrank('leaderboard', userId.toString());
```
---
**Last verified**: 2026-01-21 | **Skill version**: 2.0.0 | **Changes**: Added 5 new issues (scanIterator hang, zrange rev bug, Next.js dev server null, Vite process error, Server Actions stale cache), expanded Issues #1, #2, #7, #9 with TIER 1-2 research findings, added Version Migration Guide and Known Limitations sectionsMore General & Other skills
find-skills
vercel-labs/skills
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
1.5M
grill-me
mattpocock/skills
A relentless interview to sharpen a plan or design.
972.7k
grill-with-docs
mattpocock/skills
A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.
828.8k

