外部の無料API(利用者に課金しないデータ提供サービス)を業務システムに組み込んでいる開発者に向けた内容です。特に社内ツールや小規模サービスで、有償の商用APIを使う予算が確保できないケースを想定しています。
キャッシュのTTL(Time To Live、データを保持する有効期限)設定を誤ると、外部APIからレート制限(一定時間あたりのリクエスト数制限)を受け、画面が真っ白になる障害が起きます。個人開発のF1(フォーミュラワン)ファンサイトの実装記録に、この落とし穴と回避策が具体的に記録されていたので、業務システムの視点で整理します。
何が起きるか
このサイトでは、順位表やレース結果を取得するJolpica(コミュニティ運営のF1データAPI)と、走行中の車両テレメトリを取得するOpenF1という2つの無料APIを併用していました。
両者はデータの更新頻度が全く違います。コンストラクターの獲得ポイントは週1回程度しか変わりませんが、車のギア段数は1秒に4回変化します。
ここでTTLを間違えると、2方向の障害が起きます。1つ目は、ライブポジション表示に1時間のTTLを設定してしまい、表示が実際の状況と食い違う「嘘のライブ表示」になるパターンです。
2つ目はより深刻です。順位表のような更新頻度の低いデータに no-store(キャッシュを一切使わない設定)を指定すると、アクセスのたびに無料APIへリクエストが飛びます。
結果としてレート制限に引っかかり、API側から一時的にブロックされ、サイト全体が空白ページになるという障害につながります。
なぜ起きるか
原因を分解すると、3つの段階に分けられます。
1つ目は、キャッシュ機構への理解不足です。Next.js(Reactベースのフレームワーク)の fetch にはオプションで next: { revalidate } を渡せます。ここに秒数を渡すと、その秒数だけレスポンスをキャッシュし、再利用します。この仕組み自体は単純ですが、「このデータは何秒で古くなるか」というビジネス側の判断が抜けたまま、開発側の都合(動作確認しやすいから0秒、など)で数値を決めてしまうと事故につながります。
2つ目は、データの性質を一律に扱ってしまう設計です。順位表・スケジュール・レース結果は更新頻度が低いため長いTTL(この実装では3600秒、つまり1時間)で十分です。一方、ライブタイミングやテレメトリは更新頻度が極端に高く、15秒程度のTTLが必要になります。この違いを認識せず、全エンドポイントに同じTTLを適用すると、どちらか一方が必ず壊れます。
3つ目は、並列リクエストの制御漏れです。過去10戦分のレース結果とクイックファイをまとめて取得するような処理を Promise.all で単純に書くと、キャッシュが切れた瞬間に20本近い同時リクエストが無料APIに飛びます。無料APIは商用APIほど余裕のあるレート制限を持っていないため、これだけで簡単に制限に達します。
自分のプロジェクトが該当するか確認する方法
業務システムでも、社外の無料・低コストAPI(郵便番号検索、祝日API、為替レートAPI、株価APIなど)を使っている場合、同じ構造の落とし穴が潜んでいる可能性があります。
以下の手順で確認してみてください。
# fetch呼び出し箇所を一覧化し、revalidateやcache指定を確認する
grep -rn "fetch(" --include="*.ts" --include="*.tsx" src/ | grep -v node_modules
# revalidateオプションの有無を確認する
grep -rn "revalidate" src/
# no-storeやcache: 'no-store'が指定されている箇所を確認する
grep -rn "no-store" src/Next.jsのApp Router(app/ディレクトリを使う新しいルーティング方式)を使っているなら、next.config.js や個別のfetch呼び出しでキャッシュ挙動が上書きされていないかも見ておく必要があります。
さらに、外部API側のドキュメントで「レート制限」の項目を確認します。1分あたりの上限リクエスト数、1日あたりの上限、そして制限に達した際のHTTPステータス(多くは429 Too Many Requests)を把握しておくと、障害発生時の切り分けが速くなります。
監視の面では、429エラーの発生率をログやAPM(アプリケーション性能監視ツール)でトラッキングできているかも確認しましょう。発生してから気づくのではなく、閾値に近づいた時点で検知できる体制が理想です。
対策の手順
対策は大きく3つです。
1. データの更新頻度に応じてTTLを分類する
エンドポイントごとに「このデータは実際どのくらいの頻度で変わるか」を業務担当者やドメイン知識のある人に確認します。今回の実装では、更新頻度に応じて以下のように分けていました。
| データ種別 | 更新頻度の実態 | TTL設定 |
|---|---|---|
| 順位表・スケジュール・結果 | 週1回程度 | 3600秒 |
| SNS投稿フィード | 数分〜数時間おき | 600秒 |
| フォロワー数 | ほぼ変動なし | 1800秒 |
| ライブチャット | 常時変動 | no-store |
| 車両テレメトリ | 1秒に複数回 | 15秒 |
業務システムであれば、たとえば祝日データは年1回程度の更新で十分ですが、株価や為替は数十秒〜数分単位のTTLが必要になります。TTLの値は関数の引数として渡せるようにしておくと、後から調整しやすくなります。
2. サーバー側とクライアント側で取得経路を分ける
サーバーコンポーネント(サーバー上でレンダリングされるコンポーネント)はNext.jsのデータキャッシュを直接利用できますが、ブラウザ側のクライアントコンポーネントは同じキャッシュを共有できません。
このため、クライアント側からは自前のAPIルート(/api/openf1/[...path] のようなプロキシ)を経由させ、そこで同じTTLとトークンを再適用する構成が使われていました。こうすることで、外部APIのトークンをブラウザに露出させずに済み、CORS(クロスオリジン制約)の問題も回避できます。
3. 並列リクエストに上限を設ける
複数エンドポイントへの同時アクセスが発生する処理には、並列数を制限するユーティリティを入れます。今回の実装では、ワーカー数を3に固定した mapLimit という関数で、常に最大3本までしか同時リクエストしない設計になっていました。
加えて、1件のリクエストが失敗しても全体を止めない工夫も重要です。try/catch で個別の失敗を吸収し、失敗した1件だけを欠損データとして扱うようにすれば、外部APIの一時的な不調がページ全体の障害に広がりません。
導入前に確認すること
外部の無料・低コストAPIを業務システムに組み込む際は、次の3点を確認してから実装に入ることをおすすめします。
- 各エンドポイントのデータが実際にどのくらいの頻度で変わるかを、業務担当者に確認したか
- サーバー側・クライアント側でキャッシュ挙動が食い違っていないか、コードを
grepで確認したか - 並列リクエストに上限を設け、1件の失敗が全体に波及しない設計になっているか
この3点は、有償の商用APIに切り替える際にも無駄になりません。TTL設計と並列数制御の考え方は、レート制限のあるAPIすべてに共通する基本的な防御策です。まずは自分のコードベースで grep -rn "fetch(" を実行し、TTLが明示されていない箇所を洗い出すところから始めてみてください。