Bun の組み込み Redis クライアントを使う
Bun標準Redisクライアントの解説。接続、型付きコマンド、Pub/Sub、自動パイプライン、レート制限、ioredisへ切り替える条件を整理します。
Bun には組み込みの Redis クライアントが搭載されており、import { redis, RedisClient } from "bun" で直接インポートできます。そのため、キャッシュ・セッション・pub/sub・レート制限といった一般的なワークロードであれば、ioredis や node-redis を追加する必要はなくなりました。Bun のネイティブ Redis クライアントは Promise ベースの API を持ち、接続管理の自動化・完全型付けされたレスポンス・TLS サポートを備えており、Redis サーバーバージョン 7.2 以降に対応しています。本ガイドはその組み込みクライアントに特化した内容です。多くの「Redis with Bun」チュートリアルが暗黙的に紹介している、Bun 上で動作する ioredis については扱いません。
この区別は重要です。既存の Bun + Redis チュートリアルでよく見られるパターンは、bun add ioredis を実行して new Redis() を呼び出すというものですが、これはサードパーティライブラリであり、ランタイム API ではありません。以下の内容はすべて、Bun 自体に同梱されている API を使用します。
重要なポイント
- クライアントは
import { redis, RedisClient } from "bun"でインポートします。デフォルトのredisインスタンスはREDIS_URL、次にVALKEY_URLを参照し、どちらも設定されていない場合はredis://localhost:6379にフォールバックします。 - クライアントは最初のコマンドが実行されるまで接続を開かず、コマンドを自動的にパイプライン化し、デフォルト 10 回のリトライにわたって 50ms から最大 2000ms の指数バックオフで再接続します。
- Bun 1.3 時点で、クライアントは 66 の Redis コマンドを型付きメソッドとしてラップしています。ラップされていないコマンドは
redis.send(command, argsArray)で実行できます。これは現時点でMULTI/EXECを発行する方法でもあります。 - Pub/Sub は接続を占有するため、パブリッシュや他のコマンドの実行には
.duplicate()を呼び出して別の接続を取得します。 - Bun 1.3.x 時点では、組み込みクライアントは Redis Cluster および Sentinel をサポートしていません。これらが必要な場合は
node-redisまたはioredisを使用してください。
組み込み Redis クライアントで接続する
最も手軽な方法は、環境変数から遅延接続するデフォルトの redis エクスポートを使うことです。デフォルトでは、クライアントは REDIS_URL、次に VALKEY_URL から接続情報を読み取り、どちらも設定されていない場合は redis://localhost:6379 をデフォルトとして使用します。最初のコマンドを発行するまでソケットは開かれません。
import { redis, RedisClient } from "bun";
// デフォルトクライアント — 環境変数から REDIS_URL / VALKEY_URL を読み取る
await redis.set("hello", "world");
const result = await redis.get("hello");
// 明示的な URL を指定したカスタムクライアント
const client = new RedisClient("redis://username:password@localhost:6379");
await client.set("counter", "0");
await client.incr("counter");
アプリ全体で共有するアクセスにはデフォルトの redis インスタンスを使用し、別の接続・異なるオプション・別のデータベースが必要な場合は RedisClient を独自に構築してください。Bun Redis ドキュメントには、TLS 用の rediss:// および redis+tls://、Unix ソケット用の redis+unix:// を含む、サポートされる URL スキームの完全なリストが記載されています。
接続管理は自動で行われます。
- コマンドが実行されるまで接続は確立されません。最初のコマンドで接続が開始され、
client.close()を呼び出すまで後続のコマンドのために接続は維持されます。 - 接続が切断された場合、クライアントは自律的に回復します。50ms の遅延から始まり、試行ごとに倍増し、再接続の遅延を最大 2000ms でキャップし、
maxRetries回(デフォルト 10)まで再試行します。 - 切断中に発行されたコマンドは、
enableOfflineQueueが true(デフォルト)の場合はキューに入れられ、false の場合は即座に拒否されます。
これらは ioredis で手動で設定していた動作と同じです。
Discover how at OpenReplay.com.
コア操作と send() フォールバック
クライアントは日常的な Redis コマンドに対して型付きメソッドを公開し、レスポンスをネイティブな JavaScript の値に変換するため、手動でのパース処理をほぼ省略できます。
- 整数レスポンスは JavaScript の数値として返されます。
- バルク文字列およびシンプル文字列は文字列として返されます。
- null バルク文字列は
nullとして返されます。 - 配列は JavaScript の配列として返されます。
また、コマンド固有の型変換も行われます。EXISTS は数値ではなく真偽値を返し、SISMEMBER も真偽値を返します。
これらの動作はオプションオブジェクトで調整できます。
const client = new RedisClient("redis://localhost:6379", {
connectionTimeout: 5000, // デフォルト 10000 ms
autoReconnect: true, // デフォルト true
maxRetries: 10, // デフォルト 10
enableOfflineQueue: true, // デフォルト true
enableAutoPipelining: true, // デフォルト true
tls: true,
});
// 文字列と有効期限
await redis.set("session:123", "active");
await redis.expire("session:123", 3600); // 秒単位
const ttl = await redis.ttl("session:123");
// カウンター
await redis.set("counter", "0");
await redis.incr("counter");
await redis.decr("counter");
// exists() は真偽値を返す
const present = await redis.exists("session:123"); // true
// ハッシュ
await redis.hmset("user:123", ["name", "Alice", "email", "alice@example.com"]);
const [name, email] = await redis.hmget("user:123", ["name", "email"]);
すべてのコマンドに専用メソッドがあるわけではありません。ハッシュ・リスト・セットを含むすべての標準操作がサポートされており、Bun 1.3 時点で合計 66 コマンドが対象です。そのセット外のコマンドには send() を使用します。send メソッドは専用メソッドを持たないものを含む任意の Redis コマンドを実行でき、第一引数にコマンド名、第二引数に文字列引数の配列を渡します。
// 生コマンドのフォールバック — コマンド名 + 文字列引数の配列
await redis.send("LPUSH", ["mylist", "value1", "value2"]);
const list = await redis.send("LRANGE", ["mylist", "0", "-1"]);
send() は現時点でトランザクションを実行する方法でもあります。これについては後述するクライアントの主要な API のギャップとして取り上げます。
.duplicate() を使った Pub/Sub
Redis の Pub/Sub はサポートされていますが、サブスクライブ済みの接続は他の操作を行えません。サブスクライブは RedisClient の接続を占有するため、サブスクリプションを持つクライアントは subscribe() しか呼び出せません。他のコマンドを送信するには .duplicate() で別の接続を作成します。サブスクライブのコールバックは、最初の引数にメッセージ、2 番目の引数にチャンネルを受け取ります。
import { RedisClient } from "bun";
const redis = new RedisClient("redis://localhost:6379");
await redis.connect();
// パブリッシュや他のコマンド用の 2 つ目の接続
const subscriber = await redis.duplicate();
await subscriber.subscribe("notifications", (message, channel) => {
console.log(`[${channel}] ${message}`);
});
await redis.publish("notifications", "Hello from Bun!");
Pub/Sub は Bun 1.2.23 で追加され、ドキュメントでは現在も実験的と記載されています。実用上は安定していますが、依存する場合は Bun のバージョンを固定することをお勧めします。サブスクライブ解除には .unsubscribe() を使用します(引数なしですべてのチャンネルを解除し、チャンネルまたはリスナーを渡すことで範囲を絞れます)。
自動パイプライン化
組み込みクライアントはデフォルトでパイプライン化を行います。オプトインは不要です。クライアントはコマンドを自動的にパイプライン化し、複数のコマンドをバッチで送信してレスポンスを受信しながら処理することでパフォーマンスを向上させます。Promise.all などを使って複数の await コマンドをまとめて発行すると、それぞれが個別のラウンドトリップになるのではなく、1 回のバッチとして送信されます。
const [a, b, c] = await Promise.all([
redis.get("user:1:name"),
redis.get("user:2:name"),
redis.get("user:3:name"),
]);
自動パイプライン化は、ioredis との性能差が広がるポイントです。Bun は自社の Redis クライアントが ioredis より大幅に高速であり、バッチサイズが大きくなるほどその優位性が増すと説明しています。Bun が 1.2.9 リリースノートで公開した再現可能なベンチマーク結果がそれを裏付けています。GET ベンチマークでは、Bun.redis はバッチサイズ 10 で 44.82%、バッチサイズ 100 で 58.29%、バッチサイズ 1000 で 85.39% 高速であり、最大バッチでは約 1.85 倍の性能を示しました(固定の倍率ではありません)。(Bun 自身の 1.3 リリースブログでは、組み込みクライアントが ioredis のスループットの 7.9 倍以上を示すベンチマークチャートを掲載しており、この数値は二次的な報告ではなく Bun 自身が公表したものです。)
ステートフルなコマンドはいくつか自動パイプライン化を無効化します。AUTH・INFO・MULTI・EXEC・WATCH・SUBSCRIBE・SELECT などが該当します。パイプライン化をグローバルに無効にするには、enableAutoPipelining: false を設定してください。
実践的なパターン:キャッシュ・セッション・レート制限
以下のパターンはどの Redis クライアントでも応用できますが、ここではすべて組み込み API で実装します。1 つの小さなサービスモジュールとして参照してください。
キャッシュアサイド。 まず Redis を確認し、キャッシュミスの場合はデータベースにフォールスルーし、TTL を設定してキャッシュに格納します。
import { redis } from "bun";
async function getUser(userId: string) {
const cacheKey = `user:${userId}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const user = await db.getUser(userId); // データソース
await redis.set(cacheKey, JSON.stringify(user));
await redis.expire(cacheKey, 3600); // 1 時間
return user;
}
ライトスルーの場合は、データベースへの書き込みと同じ操作でキャッシュを更新します。stale-while-revalidate の場合は、キャッシュされた値を即座に返しつつ、TTL が近づいたらバックグラウンドでキャッシュを更新します。
TTL 付きセッションストレージ。 セッションをハッシュとして保存し、Redis に有効期限を管理させます。
async function createSession(userId: number, data: object) {
const sessionId = crypto.randomUUID();
const key = `session:${sessionId}`;
await redis.hmset(key, [
"userId", String(userId),
"created", String(Date.now()),
"data", JSON.stringify(data),
]);
await redis.expire(key, 86400); // 24 時間
return sessionId;
}
ソート済みセットを使ったスライディングウィンドウレート制限。 ソート済みセットのコマンドは専用メソッドとしてラップされていないため、send() を使う自然な場面です。ウィンドウより古いエントリを削除し、残りをカウントして、現在のリクエストを追加します。
async function rateLimit(id: string, limit = 100, windowMs = 60_000) {
const key = `ratelimit:${id}`;
const now = Date.now();
await redis.send("ZREMRANGEBYSCORE", [key, "0", String(now - windowMs)]);
const count = Number(await redis.send("ZCARD", [key]));
if (count >= limit) return { allowed: false, remaining: 0 };
await redis.send("ZADD", [key, String(now), `${now}-${crypto.randomUUID()}`]);
await redis.expire(key, Math.ceil(windowMs / 1000));
return { allowed: true, remaining: limit - count - 1 };
}
クライアントが自動パイプライン化を行うため、ZREMRANGEBYSCORE/ZCARD/ZADD/EXPIRE のシーケンスは手動のパイプラインオブジェクトなしで効率的にバッチ処理されます。
制限事項と、ioredis または node-redis を使うべき場面
組み込みクライアントは上記の一般的なユースケースをカバーしていますが、Bun 1.3.x 時点ではドキュメントに記載されたギャップがあります。トランザクション(MULTI/EXEC)は生コマンドで実行する必要があり、Redis Sentinel および Redis Cluster はサポートされていません。クラスタリング・Sentinel ベースのフェイルオーバー・Redis Stack モジュール(検索・JSON・時系列・確率的データ構造)が必要な場合は、フル機能のライブラリを使用してください。Bun 自身の移行追跡 Issue では、新規プロジェクトの優先クライアントとして node-redis を推奨し、ioredis を確立された代替として挙げています。1.3 ブログでは、クラスター・ストリーム・Lua スクリプティングのサポートが開発中であると述べられています。この領域はまだ変化が続いているため、現在の状況を確認する際はドキュメントの Limitations セクションを参照してください。
| 機能 | Bun 組み込み redis | node-redis / ioredis |
|---|---|---|
| インストール手順 | 不要 — Bun に同梱 | bun add node-redis / ioredis |
| 自動パイプライン化 | デフォルトで有効 | 設定可能 / 手動 |
| Pub/Sub | あり(実験的、.duplicate() 経由) | あり |
| トランザクション(MULTI/EXEC) | 生の send() のみ | ネイティブ API |
| Redis Cluster | 非対応 | 対応 |
| Redis Sentinel | 非対応 | 対応 |
| Redis Stack モジュール | ライブラリを使用 | node-redis が対応 |
実装上の注意として補足しておくべき点があります。ドキュメントではクライアントがネイティブコンパイルされていると説明されており(現在は Bun の 2026 年の Zig からの移行に伴い Rust で実装されていると説明されています)。ただし、この詳細はアプリケーションコードの 1 行も変えるものではありません。基盤となる言語に関わらず、API のインターフェースは同一です。
キャッシュ・セッション・pub/sub・カウンター・レート制限であれば、組み込みクライアントで十分であり、依存関係を削減できることは実質的なメリットです。Cluster・Sentinel・Redis Stack・リッチなトランザクションフローが必要になった時点で node-redis をインストールしてください。まずは import { redis } from "bun" でローカルサーバーに接続し、必要なコマンドセットがカバーされていることを確認した上で、進化する API が本番環境で予期せぬ問題を引き起こさないよう Bun のバージョンを固定してください。
よくある質問
Bun の組み込みクライアントで Redis トランザクションを実行するには?
Bun の組み込みクライアントにはネイティブの MULTI/EXEC API がないため、生の send() メソッドを通じてトランザクションを実行します。redis.send('MULTI', []) を発行し、続いてキューに入れるコマンドを実行し、最後に redis.send('EXEC', []) を発行します。MULTI・EXEC・WATCH は自動パイプライン化を無効にするため、バッチ処理されずに個別に送信されます。より豊富なネイティブトランザクション API が必要な場合は、node-redis または ioredis が対応しています。
Bun の組み込み Redis クライアントは Redis Cluster や Sentinel に対応していますか?
対応していません。Bun 1.3.x 時点では、ドキュメントの Limitations セクションに記載されているとおり、組み込みクライアントは Redis Cluster および Redis Sentinel をサポートしていません。シャードクラスタリング・Sentinel ベースのフェイルオーバー・検索・JSON・時系列などの Redis Stack モジュールが必要な場合は、フル機能のライブラリを使用してください。Bun 自身の移行 Issue では、新規プロジェクトの優先クライアントとして node-redis を推奨し、ioredis を確立された代替として挙げています。
Bun の組み込み redis インポートと Bun 上で ioredis を実行することの違いは何ですか?
組み込みクライアントは import { redis, RedisClient } from 'bun' を通じてアクセスするランタイム API であり、インストール不要でデフォルトでコマンドをパイプライン化します。ioredis を実行するとは、bun add ioredis を実行して new Redis() を呼び出すことであり、独自の接続管理を持つサードパーティライブラリです。多くの「Redis with Bun」チュートリアルは暗黙的に ioredis を使用しています。組み込みクライアントはゼロ依存で動作しますが、ioredis は組み込みクライアントが持たない Cluster・Sentinel・トランザクション API を提供します。
Bun の Redis クライアントが切断中に送信されたコマンドはどうなりますか?
enableOfflineQueue がデフォルトで true に設定されているため、切断中に発行されたコマンドはキューに入れられ、接続が回復した後に再実行されます。クライアントは 50ms から始まり試行ごとに倍増し最大 2000ms でキャップされる指数バックオフを使用して、デフォルト 10 回のリトライで自動的に再接続します。切断中にコマンドをバッファリングせず即座に拒否したい場合は、enableOfflineQueue を false に設定してください。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k