🐕

APIのリトライ戦略の重要性について

に公開

はじめに

株式会社 Rehab for JAPAN の開発チームに所属している徳永です。
システム開発でAPIをリトライする必要があり、その際に検討した内容を記事にまとめました。すでに多くのブログで取り上げられているテーマですが、改めて情報を整理する目的で執筆しています。

対象読者 (ターゲット)

この記事は、主に以下のような方を対象としています。

  • 外部APIを呼び出す際にリトライ戦略を検討している開発者
  • APIリトライのプラクティスについて知りたい方
  • TypeScriptでの実装例を参考にしたい方

背景

ある外部APIを呼び出した際にレートリミットの上限に達し、429エラー(Too Many Requests)が返却されていました。このエラーはリトライによって成功する可能性が高いため、リトライ戦略を検討するに至りました。
また、社内でも生成AI関連のAPI呼び出しなど外部APIを呼び出すケースも増えており、リトライ戦略の重要性が増していると感じています。

リトライ戦略の検討ポイント

リトライする際には以下のポイントを考慮する必要があると考えています。

  • リトライするエラーの種類
    • ネットワークエラー、タイムアウト、HTTPステータスコード(429, 500, 502, 503, 504など)
    • 例えば、429エラーはリトライ対象に含めますが、401エラー(認証エラー)はリトライしても成功しないため、対象外とするなどの方針を立てる必要があります。
    • 呼び出したAPIが429 (Too Many Requests) のステータスコードを返した際は、リトライを検討すべきです。
  • リトライ回数の上限
    • 無限リトライは避け、3回や5回など、適切な回数を設定します。
  • リトライ間隔
    • 固定間隔、指数バックオフなど
      • スクラッチで実装するよりもライブラリを利用することが多いですが、その際どのような間隔でリトライするかは開発者の判断が求められます。
  • リトライ時に副作用が発生しないか?
    • 冪等性を確認して、再実行して良いか判断することも重要です。POSTリクエストなどで副作用が発生する場合、リトライは慎重に行う必要があります。

今回の実装例

今回の実装では、TypeScriptでプロダクト開発を行っておりました。リトライ処理を実装するにあたり、工数を増やさず、既存コードへの影響を最小限に抑えることも考慮しました。

今回は、ts-retryというライブラリを使用しました。
https://github.com/franckLdx/ts-retry

このライブラリは広く知られているわけではありませんが、継続的にアップデートされメンテナンスされている点や、既存の関数をラップするだけで導入でき、後からリトライ処理を追加する今回の要件に適していたため採用しました。
実装の詳細は以下の通りです。
retryAsyncという関数を使って、API呼び出し部分をラップしている点がポイントです。

import { retryAsync } from "ts-retry"
/**
 * 指定したAPIエンドポイントに対して、リトライ処理(特にレートリミット)を組み込んでPOSTリクエストを送信する例
 * @param data 送信するデータ
 * @param delay リトライ間隔(ミリ秒)
 * @returns APIレスポンス
 */
return retryAsync(
  async () => {
    // APIエンドポイントのURL(例: "https://api.example.com/v1")
    const API_ENDPOINT = "YOUR_API_ENDPOINT"; 
    // 認証用のAPIキー
    const API_KEY = "YOUR_API_KEY"; 
    // POSTリクエストのデフォルトペイロード
    const defaultPayload = { /* ...共通の設定など... */ };

    return await fetch(`${API_ENDPOINT}/items`, { // "meetings" を "items" に変更
      method: "POST",
      headers: {
        Authorization: `Bearer ${API_KEY}`,
        "Content-Type": "application/json"
      },
      // "targetConfig" を "defaultPayload" に変更
      body: JSON.stringify({ ...defaultPayload, ...data }) 
    }).then(async (res) => {
      if (res.status === 429) {
        // 429 Too Many Requests (レートリミット超過)
        console.error("Rate limit exceeded. Retrying...")
        // リトライ処理を継続させるために undefined を返す
        return undefined
      } else if (res.status === 401) {
        // 401 Unauthorized (認証エラー)
        console.error("Unauthorized. Please check your API key.")
        // 認証エラーはリトライしても成功しないため、エラーをスローして処理を中断
        throw new Error("Unauthorized")
      }
      
      // その他のステータスコード(200 OKなど)の場合
      // レスポンスボディをJSONとしてパースして返す
      return (await res.json()) as ApiResponse 
    })
  },
  {
    delay, // リトライ間隔
    maxTry: 10, // 最大リトライ回数
    until: (lastResult) => lastResult !== undefined // 429エラーで undefined が返された場合のみリトライを継続
  }
)

おわりに

APIのリトライ戦略は、システムの信頼性とユーザー体験を向上させるために重要です。
今回の記事では、リトライ戦略の検討ポイントとTypeScriptでの実装例を紹介しました。
適切なリトライ戦略を採用することで、外部APIの呼び出しにおける失敗を減らし、システムの安定性を向上させることができるので改めて重要性を感じています。
今後もAPIリトライ戦略に関する知見を深め、より良い実装方法を模索していきたいと考えています。
この記事が、同様の課題に直面している開発者の方の参考になれば幸いです。

参考文献

Rehab Tech Blog

Discussion