api-database-upstash について
api-database-upstash は、TCP 接続を使えないサーバーレスおよびエッジ環境で、Upstash Redis、レート制限、バックグラウンドジョブを扱う開発パターンをまとめています。REST クライアント、コマンドの一括処理、トランザクションと利用上の制約を説明します。
主な機能
- 01REST/HTTP 経由で Redis を利用
- 02JSON の自動シリアライズと復元
- 03pipeline による独立コマンドの一括処理
- 04multi によるアトミックなトランザクション
- 05各種ウィンドウやトークンバケット方式の制限
- 06QStash によるバックグラウンドジョブの配信
活用シーン
- 01エッジ環境でのデータキャッシュ
- 02API ルートのレート制限
- 03Redis コマンドの一括処理による往復削減
- 04HTTP Webhook を使ったバックグラウンドジョブの予約
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ agents-inc/ skills --skill api-database-upstashインストール後、Claude Code で/api-database-upstashを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
metadata.yamlreference.mdexamples/core.mdqstash.mdrate-limiting.md詳細
Upstash のパターン
クイックガイド: Upstash は、TCP 接続を利用できないサーバーレス環境やエッジランタイム向けに設計された、REST/HTTP ベースの Redis クライアント(
@upstash/redis)を提供します。ioredis/node-redis とは異なり、すべてのコマンドが HTTP リクエストになります。永続的な接続も、接続プールも、接続の終了処理もありません。クライアントは JSON を自動的にシリアライズ/デシリアライズします(setで保存したオブジェクトは、getでオブジェクトとして返されます)。便利ですが、大きな数値やクライアント間の互換性には注意が必要です。複数のコマンドを 1 つの HTTP リクエストにまとめるにはredis.pipeline()を、アトミックなトランザクションにはredis.multi()を、すぐに利用できるレート制限アルゴリズムには@upstash/ratelimitを使用します。バックグラウンドジョブには、HTTP Webhook 経由で API にメッセージを送信する@upstash/qstashを使用します。
<critical_requirements>
重要:このスキルを使用する前に
すべてのコードは CLAUDE.md のプロジェクト規約(kebab-case、名前付きエクスポート、インポート順序、
import type、名前付き定数)に従う必要があります。
(本番コードでの初期化には必ず Redis.fromEnv() を使用してください。UPSTASH_REDIS_REST_URL または UPSTASH_REDIS_REST_TOKEN の値をハードコードしてはいけません。)
(エッジランタイムで @upstash/ratelimit のレスポンスに含まれる pending プロミスを必ず処理してください。Vercel Edge/Cloudflare Workers では context.waitUntil(pending) を使用してください。使用しないと分析データが失われます。)
(1 つのハンドラー内で独立したコマンドを 3 つ以上実行する場合は、必ず redis.pipeline() を使用してください。パイプライン化しない場合、コマンドごとに HTTP の往復が発生します。)
(Pub/Sub、ブロッキングコマンド(BRPOP、BLPOP、XREAD BLOCK)、または Lua スクリプトに Upstash を使用してはいけません。REST API はこれらをサポートしていません。代わりに TCP 接続を使用する ioredis を使用してください。)
</critical_requirements>
例
- コアパターン -- クライアントのセットアップ、コマンド、自動シリアライズ、パイプライン、トランザクション
- レート制限 -- @upstash/ratelimit のアルゴリズム、ミドルウェア、分析
- QStash -- バックグラウンドジョブ、スケジュール設定、メッセージの公開
追加リソース:
- reference.md -- コマンドのチートシート、コンストラクターのオプション、環境変数、エビクションポリシー
自動検出: Upstash、@upstash/redis、@upstash/ratelimit、@upstash/qstash、Redis.fromEnv、UPSTASH_REDIS_REST_URL、UPSTASH_REDIS_REST_TOKEN、Ratelimit.slidingWindow、Ratelimit.fixedWindow、Ratelimit.tokenBucket、サーバーレス Redis、エッジ Redis、REST Redis
使用する場面:
- TCP 接続を維持できないサーバーレス関数(AWS Lambda、Vercel、Netlify)
- HTTP のみをサポートするエッジランタイム(Cloudflare Workers、Vercel Edge、Fastly Compute)
- 事前構築済みのアルゴリズム(スライディングウィンドウ、固定ウィンドウ、トークンバケット)を使った API ルートのレート制限
- ioredis の接続プーリングが実用的でないサーバーレス/エッジ環境でのキャッシュ
- QStash を使ったバックグラウンドジョブのスケジュール設定(プッシュ型のため、長時間稼働するコンシューマーは不要)
- Upstash Global Database のリードレプリカを利用した、グローバルな読み取りレイテンシーの最適化
対象となる主なパターン:
Redis.fromEnv()とコンストラクターオプションを使った@upstash/redisクライアントのセットアップ- JSON の自動シリアライズ/デシリアライズの動作と注意点
- パイプラインによるバッチ処理(
redis.pipeline())とアトミックトランザクション(redis.multi()) @upstash/ratelimitのアルゴリズム:スライディングウィンドウ、固定ウィンドウ、トークンバケット- サーバーレスのバックグラウンドジョブとスケジュール設定のための
@upstash/qstash - Global Database のアーキテクチャ(プライマリと読み取りリージョン、結果整合性)
- エッジランタイムとの互換性と
context.waitUntil()のパターン
使用しない場面:
- 永続的な接続を使用する長時間稼働サーバー(コマンドごとのレイテンシーが低い TCP 接続には ioredis を使用してください)
- Pub/Sub、ブロッキングコマンド、Lua スクリプト(REST API はこれらをサポートしていません)
- Global Database での書き込み負荷が高いワークロード(書き込みは常にプライマリリージョンに送られます)
- コマンドごとの HTTP オーバーヘッド(約 5~15 ミリ秒)が許容できない、レイテンシーが重要な処理経路(コマンドごとに 1 ミリ秒未満の TCP 接続には ioredis を使用してください)
- 大きなペイロード(1 MB 超)――REST API にはペイロードサイズの制限があります
基本方針
Upstash は、サーバーレスおよびエッジランタイムでは TCP 接続を維持できないという課題を解決するために存在します。従来の Redis クライアント(ioredis、node-redis)は永続的な TCP ソケットに依存しているため、Cloudflare Workers では動作せず、短時間で終了する Lambda 関数では接続が切れ、ブラウザー/WebAssembly 環境では実行できません。Upstash は TCP を REST/HTTP に置き換え、コマンドごとのレイテンシー(約 5~15 ミリ秒、1 ミリ秒未満から増加)と引き換えに、あらゆる環境との互換性を実現します。
基本原則:
- 接続を必要としない設計 -- 各コマンドはステートレスな HTTP リクエストです。接続プールも、接続の終了処理も、接続数の制限もありません。これは制約ではなく、機能です。
- 自動シリアライズがデフォルト -- オブジェクトを渡せば、オブジェクトが返ります。手動で
JSON.stringify/JSON.parseを呼び出す必要はありません。これによりユースケースの 90% が簡素化されますが、生の文字列としての動作を想定する開発者には予想外の結果になることがあります。 - パフォーマンスにはパイプラインを使用 -- パイプラインを使わない場合、N 個のコマンドは N 個の HTTP リクエストになります。ラウンドトリップを減らすため、互いに独立したコマンドは必ず
redis.pipeline()でまとめてください。 - レート制限を中核機能として提供 --
@upstash/ratelimitを使えば、Lua スクリプトを書かなくても本番環境向けのアルゴリズムを利用できます。ライブラリが Redis の処理を内部で一括して行います。 - プッシュ型メッセージング -- QStash は HTTP Webhook を介して、メッセージを API に配信します。長時間稼働するコンシューマープロセスは不要なため、サーバーレスに最適です。
コアパターン
パターン 1:Redis.fromEnv() を使ったクライアントの設定
環境変数を使って初期化し、設定不要でデプロイできるようにします。コンストラクターオプションやタイムアウトの設定を含む詳しい例については、examples/core.md を参照してください。
// Good Example
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
// Reads UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN automatically
export { redis };
良い理由: 設定不要で、プラットフォーム(Vercel、Fly.io)から環境変数が注入されるため、コードにシークレットを含めずに済みます。
// Bad Example
import { Redis } from "@upstash/redis";
const redis = new Redis({
url: "https://us1-merry-cat-12345.upstash.io",
token: "AXXXAAIgcDE...",
});
悪い理由: 認証情報をハードコードするとバージョン管理システムに漏えいし、環境間で移植できません。
パターン 2:JSON の自動シリアル化
Upstash は、書き込み時に JSON.stringify、読み取り時に JSON.parse を使ってオブジェクトを自動的にシリアル化します。型安全なパターンや自動シリアル化の無効化については、examples/core.md を参照してください。
// Good Example -- objects round-trip automatically
interface UserProfile {
name: string;
email: string;
loginCount: number;
}
const CACHE_TTL_SECONDS = 3600;
await redis.set<UserProfile>(
"user:123",
{
name: "Alice",
email: "[email protected]",
loginCount: 42,
},
{ ex: CACHE_TTL_SECONDS },
);
// Returns typed object -- no JSON.parse needed
const user = await redis.get<UserProfile>("user:123");
// user is UserProfile | null
良い理由: TypeScript のジェネリクスで型安全性を確保でき、手動でシリアル化する必要がありません。TTL はオプションオブジェクトで設定できます。
// Bad Example -- unnecessary manual serialization
await redis.set("user:123", JSON.stringify({ name: "Alice" }));
const raw = await redis.get("user:123");
const user = JSON.parse(raw as string); // Double-serialized: "{\"name\":\"Alice\"}"
悪い理由: 自動シリアル化ではすでに JSON.stringify が呼び出されています。手動で実行すると二重にエンコードされた文字列になり、エスケープされた JSON として返されます。
パターン 3:パイプラインによるバッチ処理
複数のコマンドを 1 つの HTTP リクエストにまとめます。パイプラインを使わない場合、コマンドごとに個別のラウンドトリップ(各約 5~15 ミリ秒)が発生します。型付きパイプラインの結果については、examples/core.md を参照してください。
// Good Example -- single HTTP request for all commands
const USER_TTL_SECONDS = 3600;
const pipe = redis.pipeline();
pipe.set("user:123:name", "Alice", { ex: USER_TTL_SECONDS });
pipe.set("user:123:email", "[email protected]", { ex: USER_TTL_SECONDS });
pipe.incr("stats:signups");
const results = await pipe.exec<["OK", "OK", number]>();
// results[0] => "OK"
// results[1] => "OK"
// results[2] => 1 (incremented value)
良い理由: 3 つのコマンドを 1 回の HTTP ラウンドトリップで処理でき、ジェネリクスを使った型付きの結果が得られます。また、名前付きの TTL 定数を使用しています。
// Bad Example -- 3 separate HTTP requests
await redis.set("user:123:name", "Alice");
await redis.set("user:123:email", "[email protected]");
await redis.incr("stats:signups");
// 3 round-trips = ~15-45ms total vs ~5-15ms with pipeline
悪い理由: await ごとに個別の HTTP リクエストが発生します。コールドスタートの時間が重要なサーバーレス環境では、遅延が 3 倍になります。
パターン 4:アトミックトランザクション
コマンドをアトミックに実行する必要がある場合は、redis.multi() を使用します。例については、examples/core.md を参照してください。
// Good Example -- atomic counter + flag update
const tx = redis.multi();
tx.incr("order:count");
tx.set("order:last-updated", Date.now());
const [count, status] = await tx.exec<[number, "OK"]>();
良い理由: すべてのコマンドがアトミックに実行され(他のクライアントから割り込まれません)、型付きの結果が得られます。
パイプラインとトランザクションの使い分け:
- パイプライン(
redis.pipeline())— コマンドが互いに独立していて、高速化のためにバッチ処理したい場合。アトミック性は不要です。 - トランザクション(
redis.multi())— すべてのコマンドをまとめて成功させる必要があり、割り込みを許可しない場合。
パターン 5:@upstash/ratelimit によるレート制限
Redis の内部処理をすべて担う、組み込みのレート制限機能です。すべてのアルゴリズム、ミドルウェアとの統合、分析機能については、examples/rate-limiting.md を参照してください。
// Good Example
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const MAX_REQUESTS = 10;
const WINDOW_DURATION = "10 s";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(MAX_REQUESTS, WINDOW_DURATION),
analytics: true,
});
const { success, limit, remaining, reset, pending } =
await ratelimit.limit("user:123");
// CRITICAL: In edge runtimes, handle the pending promise
// context.waitUntil(pending);
if (!success) {
return new Response("Too Many Requests", {
status: 429,
headers: {
"X-RateLimit-Limit": String(limit),
"X-RateLimit-Remaining": String(remaining),
"X-RateLimit-Reset": String(reset),
},
});
}
良い理由: Lua スクリプトは不要で、制限値には名前付き定数を使用し、監視用の分析機能も利用できます。また、標準ヘッダーを含む適切な 429 レスポンスを返します。
パターン 6:QStash によるバックグラウンドジョブ
サーバーレス向けのプッシュ型メッセージングです。スケジュール設定、再試行、レシーバーの検証については、examples/qstash.md を参照してください。
// Good Example -- publish a background job
import { Client } from "@upstash/qstash";
const qstash = new Client({
token: process.env.QSTASH_TOKEN!,
});
await qstash.publishJSON({
url: "https://your-app.com/api/process-order",
body: { orderId: "order-456", action: "fulfill" },
retries: 3,
delay: "10s",
});
良い理由: ハンドラーから送信して処理を待たずに済み、失敗時には自動で再試行されます。遅延を設定でき、少なくとも 1 回の配信が保証されます。
<decision_framework>
選択の指針
Upstash と ioredis/node-redis の比較
Which Redis client should I use?
|-- Running in edge runtime (Cloudflare Workers, Vercel Edge)?
| --> @upstash/redis (only option -- no TCP available)
|-- Running in serverless (Lambda, Vercel Serverless)?
| |-- Short-lived functions with no connection reuse?
| | --> @upstash/redis (no connection management overhead)
| |-- Long-lived functions with connection pooling?
| --> ioredis (lower per-command latency)
|-- Running on a persistent server (Docker, EC2, K8s)?
| --> ioredis (persistent TCP = <1ms latency vs ~5-15ms HTTP)
|-- Need Pub/Sub, blocking commands, or Lua scripts?
| --> ioredis (REST API cannot support these)
|-- Need to run in browser or WebAssembly?
--> @upstash/redis (HTTP works everywhere)
どのレート制限アルゴリズムを選ぶべきか?
Which @upstash/ratelimit algorithm should I use?
|-- Need strict, evenly distributed limiting?
| --> slidingWindow -- smoothest, no burst-at-boundary issues
|-- Need simple, low-overhead limiting?
| --> fixedWindow -- cheapest computationally, allows boundary bursts
|-- Need to allow burst traffic up to a capacity?
| --> tokenBucket -- smooths bursts, allows initial spike up to maxTokens
|-- Need multi-region rate limiting?
--> fixedWindow (slidingWindow has high Redis command overhead in multi-region)
パイプライン、トランザクション、逐次実行の比較
How should I batch these Redis commands?
|-- Commands are independent (no ordering dependency)?
| --> Pipeline (redis.pipeline()) -- non-atomic but single HTTP request
|-- Commands must execute atomically (all-or-nothing)?
| --> Transaction (redis.multi()) -- atomic, single HTTP request
|-- Only 1-2 commands?
--> Sequential is fine -- pipeline overhead not worth it
グローバルデータベースとリージョナルデータベースの比較
Should I use Upstash Global Database?
|-- Read-heavy workload with users worldwide?
| --> Global Database -- reads from nearest replica
|-- Write-heavy workload?
| --> Regional Database -- writes always go to primary, replication doubles write cost
|-- Need strong consistency?
| --> Regional Database -- Global is eventually consistent
|-- Latency-sensitive reads from multiple continents?
--> Global Database -- sub-1ms reads from nearest region
</decision_framework>
<red_flags>
注意すべき問題
優先度の高い問題:
- オブジェクトを
redis.set()に渡す前にJSON.stringify()を使用すること。自動シリアライズがすでに処理するため、"{\"name\":\"Alice\"}"のような二重エンコードされた文字列になり、読み取り時に問題が発生します。 - エッジランタイムで
ratelimit.limit()から返されるpendingPromise を無視すること。分析データとマルチリージョン同期が通知されずに失われます。context.waitUntil(pending)を使用してください。 - パイプライン処理を使わずに、
await redis.get/set()を5回以上連続して実行すること。それぞれが個別の HTTP リクエストとなり、不要な遅延が25~75ms加わります。 - Pub/Sub(
redis.subscribe)、ブロッキングコマンド(BRPOP、BLPOP)、または Lua スクリプト(eval)を使用しようとすること。Upstash REST API はこれらをサポートしていません。TCP 接続の ioredis を使用してください。
優先度が中程度の問題:
- キャッシュキーに TTL を設定しないこと。ほかの Redis と同様に、エビクションが始まるまでメモリ使用量が無制限に増加します。
- 書き込み負荷の高いワークロードで Global Database を使用すること。書き込みは常にプライマリリージョンにルーティングされ、レプリケーションによってコマンドコストが2倍になります。
- Upstash 以外のクライアントと連携する際に
automaticDeserialization: falseを設定しないこと。他のクライアントは生の文字列を保存するため、Upstash はそれらを JSON として解析できず、エラーになります。 - リクエストごとに新しい
Redisインスタンスを作成し、モジュールレベルのシングルトンを再利用しないこと。接続レスではありますが、HTTP キープアライブとウォーム接続のメリットを得られます。
よくある間違い:
- オブジェクトを保存したのに、
redis.get()が文字列を返すと期待すること。自動デシリアライズは JSON 文字列ではなく、元のオブジェクト型を返します。 - パイプライン実行がアトミックだと思い込むこと。パイプラインはネットワーク効率のためにコマンドをまとめますが、ほかのクライアントがその間に割り込むことがあります。アトミック性が必要な場合は
redis.multi()を使用してください。 MultiRegionRatelimitでRatelimit.slidingWindowを使用すること。マルチリージョン構成ではスライディングウィンドウによって Redis コマンドのオーバーヘッドが大きくなります。代わりにfixedWindowを使用してください。- 1 MB を超える値を保存すること。REST API にはペイロードサイズの制限があります。参照を保存し、大きなデータはオブジェクトストレージから取得してください。
注意点とエッジケース:
- 大きな数値は文字列になります:JavaScript では
2^53 - 1(Number.MAX_SAFE_INTEGER)を超える数値を安全に扱えません。TypeScript の型がnumberでも、Upstash はこれらを文字列として返します。大きな数値は必ず検証してください。 - デフォルトで Base64 エンコードされます:SDK はエッジケースに対応するため、Base64 エンコードされたレスポンスを要求します。
dmFsdWU=のように出力が文字化けしている場合は、レスポンスエンコーディングが影響しています。responseEncodingオプションを確認してください。 - 存在しないキーに対して、
redis.get()はundefinedではなくnullを返します:これは TypeScript の型の絞り込みに関わります。truthiness ではなくresult !== nullを確認してください。 - SET オプションには位置引数ではなくオブジェクトを使用します:Upstash では
redis.set("key", "value", { ex: 300 })を使用します。redis.set("key", "value", "EX", 300)のようにはしません。ioredis の位置引数形式は機能しません。 - Global Database は結果整合性です:書き込み直後に別のリージョンから読み取ると、古いデータが返される場合があります。結果整合性を前提に設計するか、強い整合性が必要な場合はリージョナルデータベースを使用してください。
- 存在しないキーに対して
hgetallは空のオブジェクト{}を返します:result === nullではなくObject.keys(result).length === 0を確認してください。 - Cloudflare Workers では
blockUntilReady()は動作しません:Cloudflare ではDate.now()の動作が異なります。代わりにlimit()と手動の再試行ロジックを使用してください。 - WATCH コマンドはありません:Upstash REST API は楽観的ロックに使う
WATCHをサポートしていません。アトミックな操作にはredis.multi()を使用するか、アプリケーションレベルの楽観的並行制御を実装してください。 - 自動パイプライン処理を利用できます:コンストラクターで
enableAutoPipelining: trueを指定すると、同じイベントループのティック中に発行されたコマンドを SDK が自動的にまとめられます。
重大な注意事項
すべてのコードは CLAUDE.md のプロジェクト規約(kebab-case、名前付きエクスポート、インポート順序、
import type、名前付き定数)に従う必要があります。
(本番コードでの初期化には、必ず Redis.fromEnv() を使用してください。UPSTASH_REDIS_REST_URL または UPSTASH_REDIS_REST_TOKEN の値をハードコードしてはいけません。)
(エッジランタイムでは、@upstash/ratelimit のレスポンスに含まれる pending プロミスを必ず処理してください。Vercel Edge/Cloudflare Workers では context.waitUntil(pending) を使用してください。使用しないと分析データが失われます。)
(1つのハンドラー内で独立したコマンドを3つ以上発行する場合は、必ず redis.pipeline() を使用してください。パイプラインを使用しない場合、各コマンドで個別にHTTPの往復が発生します。)
(Pub/Sub、ブロッキングコマンド(BRPOP、BLPOP、XREAD BLOCK)、LuaスクリプトにUpstashを使用してはいけません。REST APIではこれらをサポートしていないため、代わりにTCP接続でioredisを使用してください。)
これらのルールに従わないと、認証情報の漏えい、エッジランタイムでの気付かないデータ損失、HTTPリクエストの逐次実行による不要な遅延、未サポートのコマンドによるランタイムエラーが発生します。
</critical_reminders>