
Lambda Web Functions のキャッシュを ElastiCache Serverless for Valkey に置いてみた
はじめに
先日、DevelopersIO のバックエンドを、ECS(EC2)から Lambda Web Functions へ移行しています。
ECS(EC2)で動かしていたときは、Next.js のメモリキャッシュとファイルキャッシュを使って、CMS(Contentful)の Content Delivery API(CDA)の呼び出しを抑えていました。Lambda Web Functions への移行では、実行環境(インスタンス)が増減や入れ替えで消失するため、Next.js 側のキャッシュは廃止しました。
代わりに、CDA の応答をキャッシュする、Next.js とは別の Lambda Web Functions(valkey-cda-mr。以降、キャッシュ Lambda)と、Amazon ElastiCache Serverless for Valkey(以降、Valkey Serverless)を置きました。
構成
全体の流れ

赤枠が、今回置いた部分です。Next.js は Contentful CDA の代わりに、CDA 用の CloudFront を呼びます。その先のキャッシュ Lambda は、同じリージョンの Valkey Serverless を読み、値が無いときだけ Contentful CDA から取得します。
実行環境の寿命
Lambda Web Functions の実行環境は、環境ごとにメモリが 2 GB 固定で、増減や入れ替えで消失します。
キャッシュ Lambda のログで、直近1週間(5リージョン)に終了した約480環境の稼働時間を集計しました。稼働時間は、起動から最後にリクエストを処理するまでの時間で、中央値は約30分、最長は約6.5時間でした。
| 稼働時間(分) | ap-northeast-1 | us-east-1 | us-east-2 | us-west-2 | eu-central-1 | 計 | 割合 |
|---|---|---|---|---|---|---|---|
| 1未満 | 7 | 3 | 6 | 6 | 9 | 31 | 6% |
| 1〜5 | 11 | 13 | 10 | 7 | 5 | 46 | 9% |
| 5〜10 | 51 | 27 | 19 | 2 | 0 | 99 | 20% |
| 10〜20 | 14 | 13 | 7 | 2 | 1 | 37 | 8% |
| 20〜30 | 9 | 12 | 5 | 0 | 0 | 26 | 5% |
| 30〜60 | 14 | 8 | 3 | 0 | 1 | 26 | 5% |
| 60〜120 | 16 | 26 | 8 | 4 | 3 | 57 | 12% |
| 120〜240 | 12 | 17 | 17 | 6 | 6 | 58 | 12% |
| 240〜330 | 6 | 4 | 7 | 2 | 5 | 24 | 5% |
| 330〜360 | 0 | 4 | 3 | 3 | 3 | 13 | 3% |
| 360〜375 | 8 | 5 | 3 | 10 | 9 | 35 | 7% |
| 375〜390 | 5 | 5 | 6 | 9 | 8 | 33 | 7% |
| 計 | 153 | 137 | 94 | 51 | 50 | 485 | 100% |
10分未満で終了した環境が約3分の1、6時間以上動いた環境が1割強で、稼働時間は短い環境と約6時間の環境に分かれました。
環境内のメモリやファイルに置いたキャッシュは、環境と一緒に消失します。そこで、各リージョンの Valkey Serverless を、そのリージョンの全環境が使うデータストアにして、環境が入れ替わってもキャッシュを保てるようにしました。Valkey Serverless に置いた Contentful CDA の応答は、古さに応じて次の状態に分けて扱います。
| 状態 | 応答の古さ | 動き |
|---|---|---|
| HIT | 新鮮 | そのまま返す |
| STALE | 古い | そのまま即答し、裏で更新する(stale-while-revalidate) |
| EXPIRED | さらに古い | Contentful CDA から取得し直してから返す |
| MISS | Valkey Serverless に無い | Contentful CDA から取得して返す |
Valkey への接続
この構成では、Valkey Serverless のパブリックエンドポイントに、IAM 認証と TLS 1.3 で接続します。パブリックエンドポイントでは、操作のたびに接続し直すと、接続を使い回した場合より p50 が約200倍遅くなります。
そこでキャッシュ Lambda は、接続を環境の起動時に始め、確立した1本を全リクエストで使い回します。
cache.mjs の抜粋です。
let client = null;
let connecting = null; // 接続中の Promise(接続中だけ非 null)
let retryAt = 0;
/** モジュール読み込み時に接続を開始する(最初のリクエストより前に完了させる) */
export function start() {
connect();
}
接続が済んでいない間の扱いは、get 関数にあります。
/**
* 接続済みのクライアントを返す。接続の確立中なら waitMs まで待つ(コールドスタート直後の 1 回目用)。
* 接続に失敗している間は待たずに null を返す。
*/
async function get(waitMs) {
if (client) return client;
if (!connecting && Date.now() >= retryAt) connect();
if (connecting && waitMs > 0) {
await Promise.race([connecting, new Promise((r) => setTimeout(r, waitMs))]);
}
return client;
}
実測
Valkey Serverless の値はすべて東京のものです。
ヒット率
期間は直近1週間です。東京の Valkey Serverless の CacheHitRate(Valkey の GET で値が見つかった割合。古すぎて取得し直した分を含む)は、1時間平均でおおむね7〜9割を保ちました。

応答時間
期間は直近1日です。キャッシュ Lambda 内で測った応答時間の p50 を、状態別に示します。値は、cda.request.duration のヒストグラムから求めた推定値で、5リージョン合算です。
| 状態 | 関数内の応答時間(p50) |
|---|---|
| HIT | 約 1 ms |
| STALE | 約 1 ms |
| MISS | 約 130 ms |
| EXPIRED | 約 120 ms |
費用
期間は直近1週間です。東京の Valkey Serverless の費用は、Cost Explorer のオンデマンド換算で、1日約 1.2 USD、月に直すと約 40 USD でした。
| 項目 | 1日あたり(オンデマンド換算) | 内訳 |
|---|---|---|
| 保存分 | 約 1.2 USD | 約 12 GB 時(平均約 0.5 GB) |
| ECPU 分 | 0.01 USD 未満 | 約 200万 ECPU/日 |
| 合計 | 約 1.2 USD |
Valkey Serverless の課金は、保存したデータ量(GB 時、最小 100 MB)と ECPU で決まり、ノード料金はありません。
ノード料金が無く、費用のほぼ全部が保存分なので、リージョンごとに Valkey Serverless を置いても固定費は小さく済みます。
アクセスの急増時
期間は 10/30 の夜です。キャッシュ Lambda のリクエスト数は、ピークで1分あたり約 4,600件(約 80 rps)になりました。平常時は1分あたり約 600件で、ピークは約8倍です。5リージョンとも 5XX は0件で、Valkey Serverless のエラー(接続とコマンド)も0件でした。

ピークの1時間は約14万件で、平常時の1分あたり約 600件を1時間に直した約3.5万件の約4倍です。
まとめ
ECS(EC2)で Next.js のメモリとファイルに置いていたキャッシュを、Lambda Web Functions では Valkey Serverless に置き換えました。記事の更新日を見て TTL を変えるなど、キャッシュの持ち方も柔軟になりました。
Lambda Web Functions は、実行環境のメモリやストレージの容量、連続して動く時間に制限があります。Valkey Serverless はノード料金が無くリージョンごとに置きやすいので、マルチリージョン構成でこれらを補う手段の1つとして試してみてください。










