fxサイトをvercelにデプロイしているのですが、政策金利をAPIで取得するコンテンツを作ろうとした際に、リクエストごとにAPIを叩くのは冗長なので、バッチで実行しようと考えたのですが、vercelではcronが一つしか使えないため(他のプロジェクトで利用)、ISRとオンデマンド再検証で実装した、と言う詳細の記事になります。
FXサイトでは、政策金利のように「頻繁には変わらないが、最新値は表示したい」データを扱うことがあります。
このようなデータをユーザーのリクエストごとにFRED APIへ取りに行くと、次のような問題が起きます。
- 同じデータを何度も取得するため冗長
- FRED APIへのリクエスト数が増える
- FRED側の障害や一時的な通信エラーの影響を、ユーザーが直接受ける
- レスポンス速度が外部APIの速度に左右される
そこで、Vercel Cronの代わりに次の2つを組み合わせています。
- ISR(Incremental Static Regeneration)による6時間ごとの自動更新
- オンデマンド再検証による手動更新
この記事では、この構成とリクエストの流れを整理します。
全体像
基本的な考え方は、FRED APIのレスポンスをキャッシュし、通常のアクセスではそのキャッシュを返すことです。
利用者
│
│ GET /api/rates
▼
保存済みの政策金利JSON
│
├─ キャッシュが有効(6時間以内)
│ └─ 保存済みJSONを返す
│
└─ キャッシュが古い(6時間経過後)
├─ まず古いJSONを返す
└─ 裏側でFRED APIから再取得して保存
管理者
│
│ POST /api/rates/revalidate
│ Authorization: Bearer <シークレット>
▼
キャッシュを古い状態として扱う
│
└─ 次のアクセスをきっかけに再取得
重要なのは、ユーザーへのレスポンスと、キャッシュの更新を分離していることです。
FRED APIの取得に失敗しても、直前まで正常だったデータをすぐに消さないため、画面には古い値を表示し続けられます。
1. ISRによる6時間ごとの自動更新
政策金利APIの実装では、src/app/api/rates/route.tsに再検証間隔を指定しています。
export const revalidate = 21600;
21600秒は6時間です。
つまり、/api/ratesのレスポンスは最大6時間キャッシュされます。
キャッシュが有効な場合
6時間以内にアクセスされた場合は、毎回FRED APIを呼び出しません。
GET /api/rates
│
└─ キャッシュが6時間以内
│
└─ 保存済みのJSONを返す
これにより、同じデータに対する不要な外部APIリクエストを減らせます。
6時間を過ぎた場合
キャッシュの有効期限を過ぎた後、最初にアクセスが発生すると、次のように動作します。
- まず、直前まで保存されていた古いJSONを返す
- レスポンスの裏側でFRED APIへデータを取りに行く
- 取得に成功したら、新しいJSONをキャッシュへ保存する
- 次回以降のアクセスでは、新しいデータを返す
最初のアクセス
├─ 直前のキャッシュを返す
└─ 裏側でFRED APIから再取得
└─ 成功したらキャッシュを更新
次のアクセス
└─ 新しいキャッシュを返す
この仕組みは、いわゆるstale-while-revalidateに近い動作です。
ユーザーをFRED APIの待ち時間に巻き込まず、古いデータを表示しながらバックグラウンドで更新します。
再取得に失敗した場合
FRED APIからの取得に失敗した場合は、処理中に例外を投げます。
ここで重要なのは、失敗したデータで正常なキャッシュを上書きしないことです。
FRED APIからの取得に失敗
│
├─ 前回の正常なキャッシュは維持
└─ 次のアクセス時に再度取得を試みる
そのため、一時的にFRED APIが落ちていても、画面からデータが消えることを防げます。
この構成では、アクセスそのものが更新のきっかけになります。
定期実行のためにVercel Cronを使わなくても、サイトへのアクセスがある限り、古くなったデータは段階的に更新されます。
2. オンデマンド再検証による手動更新
ISRは便利ですが、更新間隔が6時間なので、管理者が「今すぐ更新したい」と思ったときには待ち時間が発生します。
そこで、手動でキャッシュを古い状態にできるAPIを用意しています。
POST /api/rates/revalidate
このAPIを正しいシークレット付きで呼び出すと、次の処理が実行されます。
revalidateTag("policy-rates", "max");
policy-ratesは、政策金利データに付けているキャッシュタグです。
オンデマンド再検証では、このタグを指定して、政策金利データだけを再検証対象にします。サイト全体を再ビルドしたり、関係のないキャッシュまで削除したりする必要はありません。
オンデマンド再検証の流れ
管理者
│
│ POST /api/rates/revalidate
│ Authorization: Bearer <REVALIDATE_SECRET>
▼
policy-ratesタグを再検証対象にする
│
└─ 次の /api/rates へのアクセスで再取得
ここでも、再検証を呼び出した瞬間に、ユーザーへ空のデータを返すわけではありません。
次のアクセスでは、前回のキャッシュを利用しながら、裏側でFRED APIから最新データを取得します。取得に成功すれば、その後のアクセスから新しいデータが使われます。
そのため、FRED APIが一時的に落ちていても、直前の正常なデータを表示し続けられます。
再検証APIをシークレットで保護する
再検証APIは、誰でも呼び出せる状態にしてはいけません。
認証なしで公開すると、第三者が何度も再検証APIを呼び出し、FRED APIへのリクエストを意図的に増やせるためです。
そこで、環境変数REVALIDATE_SECRETを使います。
.envの例です。
REVALIDATE_SECRET=xxxxxxxxxxxxxxxx
この値はGitへコミットしません。.envは.gitignoreに追加済みです。
本番環境では、ローカル開発用とは別の値を作成し、Vercelの環境変数にも同じ名前で設定します。
名前: REVALIDATE_SECRET
値: 本番用に作成したシークレット
再検証APIを呼び出すときは、AuthorizationヘッダーにBearerトークンとして渡します。
curlで手動更新する
シークレットをコマンドに直接書かず、シェルの環境変数から渡す方法がおすすめです。
export REVALIDATE_SECRET='ここにシークレットを設定'
curl -X POST \
-H "Authorization: Bearer ${REVALIDATE_SECRET}" \
https://****.vercel.app/api/rates/revalidate
シークレットをシェルの環境変数に入れておけば、コマンド履歴や共有画面に直接表示されるリスクを減らせます。
シークレットをローテーションする場合は、Vercelの環境変数を更新したあとに再デプロイし、呼び出し側の環境変数も更新します。
なぜ政策金利は「翌日に更新」すればよいのか
FREDのデータが更新されるタイミングと、中央銀行が政策金利を決定するタイミングは必ずしも同じではありません。
政策金利の発表直後は、FRED側のデータがまだ更新されておらず、再検証APIを呼び出しても同じ値が返ることがあります。
そのため、手動更新を行う場合は、発表直後に何度も実行するよりも、翌日に一度呼び出すほうが効果的です。
もちろん、何もしなくても、ISRによって最長6時間程度でアクセスをきっかけに自動更新されます。
この構成のメリット
FRED APIへのリクエストを削減できる
6時間以内のアクセスでは、保存済みのレスポンスを返すため、ユーザー数が増えてもリクエストごとにFRED APIを呼び出す必要がありません。
レスポンスを速くしやすい
通常のアクセスでは外部APIのレスポンスを待たず、キャッシュ済みのJSONを返せます。
外部API障害に強い
再取得に失敗しても、前回の正常なデータを残すため、画面に何も表示できなくなる事態を避けられます。
Vercel Cronを使わずに済む
定期実行のためのCron枠を消費せず、アクセスをトリガーにしてデータを更新できます。
必要なときだけ即時更新できる
政策金利の発表後など、管理者が必要と判断したタイミングでは、オンデマンド再検証APIを呼び出して更新を促せます。
注意点
この方式では、アクセスがまったくない時間帯に自動更新が実行されるわけではありません。
6時間を超えた時点で、必ずバックグラウンド処理が走るという意味ではなく、有効期限を過ぎた後の最初のアクセスが更新のきっかけになるという仕組みです。
また、オンデマンド再検証も、キャッシュを無条件に最新データへ置き換える機能ではありません。再検証後のアクセスでFRED APIからデータを取得し、成功した場合に新しいキャッシュが作られます。
つまり、次のように考えると分かりやすいです。
- ISR:6時間経過後のアクセスをきっかけに更新する
- オンデマンド再検証:管理者がキャッシュを古い状態にし、次のアクセスで更新を促す
- FRED APIが失敗:前回の正常なデータを維持する
まとめ
政策金利のように、数秒単位のリアルタイム性は必要ないものの、定期的に更新したいデータには、リクエストごとの外部API呼び出しは適していません。
今回の構成では、次のように役割を分担しています。
通常時
└─ キャッシュ済みJSONを返す
6時間経過後のアクセス
└─ 古いJSONを返しつつ、裏側で再取得する
管理者による手動更新
└─ revalidateTagでキャッシュを古い状態にする
FRED APIの障害
└─ 前回の正常なデータを維持する
Vercel Cronの枠に余裕がない場合でも、ISRとオンデマンド再検証を組み合わせれば、APIリクエスト数を抑えながら、必要十分な頻度でデータを更新できます。参考になれば幸いです。