vercel-kv について
vercel-kvは、@vercel/kv経由でRedis互換ストアを使うVercel開発者向けのスキルです。キャッシュ、レート制限、セッション管理の例と、一般的な制約や問題への対処を紹介します。
主な機能
- 01@vercel/kvによるキー値データの読み書き
- 02一時データの有効期限設定
- 03パイプラインによる操作の一括実行
- 04シリアライズ、型、スキャンの問題への対処
活用シーン
- 01データベースクエリ結果のキャッシュ
- 02リクエスト頻度の制限
- 03有効期限付きセッションの保存
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill vercel-kvインストール後、Claude Code で/vercel-kvを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.md.claude-plugin/plugin.jsonassets/example-template.txtreferences/example-reference.mdscripts/example-script.shtemplates/package.jsonsession-management.tssimple-rate-limiting.ts詳細
Vercel KV
最終更新日:2026-01-21 バージョン:@vercel/[email protected](Redis 互換、Upstash によって提供)
クイックスタート
# 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
基本的な使い方:
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');
重要:常に名前空間付きのキー(123 ではなく user:123)を使用し、一時データには TTL を設定してください。
よくあるパターン
キャッシュ(キャッシュアサイド):
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;
レート制限:
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
}
セッション管理:
const sessionId = crypto.randomUUID();
await kv.setex(`session:${sessionId}`, 7 * 24 * 3600, { userId });
パイプライン(バッチ処理):
const pipeline = kv.pipeline();
pipeline.set('user:1', data);
pipeline.incr('counter');
const results = await pipeline.exec(); // Single round-trip
キーの命名:user:123、post:abc:views、ratelimit:ip:endpoint のような名前空間を使用してください。
重要なルール
必ず行うこと:
- ✅ 一時データには TTL を設定する(
setではなくsetex) - ✅ 名前空間付きのキーを使用する(
123ではなくuser:123) - ✅ null の戻り値(存在しないキー)を処理する
- ✅ バッチ処理にはパイプラインを使用する
絶対に行わないこと:
- ❌ TTL の設定を忘れる(メモリリーク)
- ❌ 1 MB を超える大きな値を保存する(Vercel Blob を使用してください)
- ❌ KV をプライマリデータベースとして使用する(キャッシュです)
- ❌ JSON にシリアライズできないデータ(関数、BigInt、循環参照)を保存する
既知の問題の防止
このスキルは、文書化されている15件の問題を防止します。
問題 #1:環境変数の不足
エラー:Error: KV_REST_API_URL is not defined または KV_REST_API_TOKEN is not defined
出典:https://vercel.com/docs/storage/vercel-kv/quickstart | GitHub Issue #759
発生する理由:ローカル環境またはデプロイ環境で環境変数が設定されていません。モノレポ(Turborepo/pnpm ワークスペース)では、@vercel/kv を共有パッケージに抽象化すると、ローカルビルドは成功するにもかかわらず Vercel のビルドが失敗することがあります。
防止策:vercel env pull .env.local を実行し、.env.local が .gitignore に含まれていることを確認してください。モノレポの場合は、(1)共有パッケージではなく利用するアプリ内でクライアントを作成する、(2)Vercel の環境変数 UI を使用してプロジェクトレベルで設定する、または(3)turbo.json のパイプライン設定に環境変数を追加する方法があります:{ "pipeline": { "build": { "env": ["KV_REST_API_URL", "KV_REST_API_TOKEN"] } } }。
問題 #2:JSON シリアライズエラー
エラー:TypeError: Do not know how to serialize a BigInt または循環参照エラー。また、hset() は数値文字列を数値に変換します。
出典:https://github.com/vercel/storage/issues/89 | GitHub Issue #727
発生する理由:JSON にシリアライズできないデータ(関数、BigInt、循環参照)を保存しようとしています。さらに、hset() を使って数値のように見える文字列(例:'123456')を保存すると、hgetall() はそれらを数値として返すため、型の一貫性が損なわれます。
防止策:プレーンオブジェクト、配列、文字列、数値、ブール値、null のみを保存してください。BigInt は文字列に変換してください。数値文字列をハッシュフィールドに保存する場合は、(1)'code_123456' のような数値以外の接頭辞を使用する、(2)JSON 文字列として保存し、取得後に解析する、または(3)型を検証して再キャストする(hgetall() の後に String(value.field) を使用する)方法があります。
問題 #3:キー名の衝突
エラー:予期しないデータが返される、または別の機能によってデータが上書きされる
出典:本番環境でのデバッグ、ベストプラクティス
発生する理由:異なる機能で cache、data、temp のような汎用的なキー名を使用しています。
防止策:常に名前空間付きのキーを使用してください。形式は feature:id:type です。
問題 #4:TTL が未設定
エラー:メモリ使用量が際限なく増加し、古いデータが期限切れにならない
出典:Vercel KV のベストプラクティス
発生する理由:一時データに setex() ではなく set() を使用しています。
防止策:すべての一時データに setex(key, ttl, value) を使用してください。適切な TTL(秒)を設定してください。
問題 #5:レート制限の超過(無料プラン)
エラー:Error: Rate limit exceeded またはコマンドの失敗
出典:https://vercel.com/docs/storage/vercel-kv/limits
発生する理由:無料プランの月間30,000コマンドの上限を超えています。
防止策:Vercel ダッシュボードで使用状況を監視し、必要に応じてプランをアップグレードしてください。キャッシュを使用して KV の呼び出しを減らしてください。
問題 #6:大きな値の保存
エラー:Error: Value too large またはパフォーマンスの低下
参照元:https://vercel.com/docs/storage/vercel-kv/limits
発生する理由:KV に 1MB を超える値を保存しようとしているためです。
予防策:ファイルや画像には Vercel Blob を使用してください。KV の値は小さく(100KB 未満を推奨)保ってください。
問題 #7:Get 時の型の不一致
エラー:TypeScript エラー、実行時の型エラー。ジェネリックな kv.get<T>() は、データが存在していても null を返すことがあります。
参照元:一般的な TypeScript の問題 | GitHub Issue #510
発生する理由:kv.get() は unknown 型を返すため、キャストまたは検証が必要です。さらに、シリアライズ/デシリアライズの問題により、CLI ではデータが存在すると表示されているのに、kv.get<T>() のようなジェネリックを使用すると関数が null を返すことがある、型推論のバグもあります。
予防策:get() ではジェネリックを使用しないでください。代わりに、型パラメーターを指定せずに取得してからキャストします:const rawData = await kv.get('key'); const data = rawData as MyType | null;。使用前に Zod または型ガードで検証してください。
問題 #8:Pipeline エラーが処理されない
エラー:サイレントな失敗、一部のみ実行される
参照元:https://github.com/vercel/storage/issues/120
発生する理由:Pipeline の実行では、個々のコマンドが失敗することがあります。
予防策:pipeline.exec() の結果配列を確認し、エラーを処理してください。
問題 #9:Scan 操作の非効率
エラー:クエリの遅延、タイムアウトエラー。v3.0.0 以降、カーソルの型が number から string に変更されました。
参照元:Redis のベストプラクティス | リリースノート v3.0.0
発生する理由:大規模なデータセットに対して scan() を使用している、またはカーソルを誤って処理しているためです。バージョン 3.0.0 では破壊的変更が導入され、scan のカーソルは number ではなく string になりました。
予防策:count パラメーターを制限し、カーソルを使って正しく反復処理し、本番環境では全件スキャンを避けてください。v3.0.0 以降では、let cursor: string = "0" を使用し、cursor !== "0"(!== 0 ではありません)で比較してください。
問題 #10:TTL の更新漏れ
エラー:セッションの期限切れが早すぎる、キャッシュが予定より早く無効になる
参照元:本番環境でのデバッグ
発生する理由:アクセス時に TTL を更新していない(スライディング有効期限)ためです。
予防策:アクセス時に expire(key, newTTL) を使用して、スライディングウィンドウを実装してください。
問題 #11:scanIterator() の無限ループ(v2.0.0 以降)
エラー:kv.scanIterator() の使用時に for await ループが終了しない
参照元:GitHub Issue #706
発生する理由:イテレーターが完了を正しく通知しない、v2.0.0 以降のバグです。イテレーターはキーを正しく処理しますが終了しないため、関数が戻りません。また、sscanIterator() にも影響します。
予防策:scanIterator() の代わりに、カーソルを使った手動の scan() を使用してください。
// 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");
問題 #12:rev: true を指定した zrange() が空の配列を返す
エラー:データが存在するのに、kv.zrange(key, 0, -1, { rev: true }) が空の配列を返す
参照元:GitHub Issue #742
発生する理由:特定のキーのパターンで reverse フラグを処理する際の SDK のバグです。CLI は常に正しい値を返します。rev フラグを削除すると、データは正しく返されます。
予防策:rev フラグを省略してメモリー上で逆順にするか、代わりに zrevrange() を使用してください。
// 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);
問題 #13:Next.js 開発サーバーで最初の呼び出しが null を返す
エラー:開発サーバーの起動後、最初の呼び出しでは kv.get() が null を返し、以降の呼び出しでは正しいデータを返す
参照元:GitHub Issue #781
発生する理由:Next.js の静的レンダリングが最初のレスポンスをキャッシュするためです。Next.js 開発サーバーでコンパイルが再起動するたびに発生します。
予防策:unstable_noStore() で動的レンダリングを強制する、fetch 呼び出しで cache: 'no-store' を使用する、またはリトライロジックを追加してください。
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」エラー
エラー:Vite で @vercel/kv をインポートすると、Uncaught ReferenceError: process is not defined が発生します
出典:GitHub Issue #743
発生する理由:Vite はデフォルトで Node.js の process.env をポリフィルしません。@vercel/kv は process.env が存在することを前提としています。
予防策:Vite の define を使ってポリフィルするか、import.meta.env とともに createClient を使うか、vite-plugin-node-polyfills をインストールしてください。
// 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 が古いデータを返す
エラー:KV の値を更新した後も、Server Actions 内の kv.get() が古い、またはキャッシュされたデータを返します
出典:GitHub Issue #510
発生する理由:Next.js の静的レンダリングが Server Action のレスポンスをキャッシュします。コンソール出力が黄色で表示されます(キャッシュヒットのインジケーター)。
予防策:unstable_noStore() を使って動的レンダリングを強制するか、Server Actions の代わりにルートハンドラーを使ってください。
// 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 });
}
バージョン移行ガイド
v3.0.0 の破壊的変更
カーソルの型が変更されました(リリースノート):
- スキャンカーソルの戻り値が
numberからstringに変更されました - 比較を
cursor !== 0からcursor !== "0"に変更してください
// 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 の破壊的変更
自動パイプライン処理がデフォルトで有効になりました(リリースノート):
- パフォーマンス向上のため、コマンドが自動的にバッチ処理されるようになりました
- エッジケースでタイミングの問題が発生する場合があります
- 必要に応じて
enableAutoPipelining: falseで無効にしてください
// 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() のバグが導入されました:v2.0.0 以降では、scanIterator() に無限ループのバグがあります。回避策については Issue #11 を参照してください。
既知の制限事項
Redis Streams はサポートされていません
出典:Vercel KV Redis Compatibility | GitHub Issue #278
基盤となる Upstash Redis がサポートしているにもかかわらず、@vercel/kv は Redis Streams(XREAD、XADD、XGROUP など)をサポートしていません。このパッケージにはストリーム用のメソッドがありません。
回避策:ストリームには @upstash/redis を直接使ってください。
// @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');
ハッシュ操作は文字列ではなくオブジェクトを返します
kv.hgetall() は JSON 文字列ではなく、JavaScript オブジェクトを返します。文字列としてシリアライズされると想定している開発者を混乱させます。
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);
高度なパターン
分散ロック(競合状態を防止):
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);
}
}
リーダーボード(ソート済みセット):
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());
最終確認日:2026-01-21 | スキルのバージョン:2.0.0 | 変更内容:新しい Issue を 5 件追加(scanIterator の停止、zrange の rev バグ、Next.js 開発サーバーでの null、Vite の process エラー、Server Actions の古いキャッシュ)、TIER 1~2 の調査結果に基づいて Issue #1、#2、#7、#9 を拡充、「バージョン移行ガイド」と「既知の制限事項」のセクションを追加