【実務・中級編】 HTTPステータスコード429 (Too Many Requests) とRetry-Afterヘッダー – Web APIアーキテクチャ・データ連携実践ガイド

429 Too Many Requestsの深淵 —— APIの「優しさ」を実装するRetry-Afterの流儀

ネットワークエンジニアとして数々のAPIサービス運用や大規模トラフィックのトラブルシューティングを経験してきましたが、API設計において最も「設計者の品格」が問われるのは、成功時のレスポンスではなく、「限界に達したときの拒絶の仕方」だと確信しています。

APIのレスポンスが200 OKで返り続ける間は、誰だって美しいコードを書けます。しかし、システムが悲鳴を上げているとき、いかにクライアントをなだめ、健全な状態へ復帰させるか。そのための強力なツールが HTTP 429 Too Many Requests と Retry-After ヘッダーです。

今日は、RFC 6585が定義するこのステータスコードを、単なる「エラー」ではなく、システムを保護し、トラフィックを平滑化するための「制御信号」として使いこなす術を伝授します。

—

1. なぜ「429」を適切に返すことが重要なのか

多くの初心者が陥る罠は、レートリミット超過時にステータスコード 403 Forbidden を返してしまうことです。あるいは、何も考えずにコネクションを強制遮断してTCP RSTを返すこともあるでしょう。

しかし、403 は「権限がない」ことを示します。クライアント側のロジックが「権限エラーだから再試行しても無駄だ」と判断し、本来なら数秒待てば回復するはずの処理を、永続的なエラーとして処理終了させてしまう可能性があります。

429 Too Many Requests は、クライアントに対して「今はダメだが、少し時間を置けば成功する可能性がある」という、極めて建設的なメッセージを送るためのコードです。

2. Retry-After:クライアントへの「礼儀」

ただ「429」を返すだけでは、クライアントは「いつ再試行すればいいのか」が分かりません。ここで登場するのが Retry-After ヘッダーです。このヘッダーこそが、API設計における「優しさ」の正体です。

Retry-After には、主に2種類の値を指定できます。

  • 秒数指定 (整数): 「あと何秒待てばよいか」を指定します。(例: Retry-After: 30)
  • HTTP日付 (RFC 7231形式): 「いつまで待てばよいか」を絶対時間で指定します。(例: Retry-After: Wed, 21 Oct 2023 07:28:00 GMT)

実務では、クライアント側の実装の容易さを考慮すると、秒数指定(Delay-seconds)の方が扱いやすく、一般的です。

—

3. 実践:429を正しくハンドリングする実装例

Pythonでの実装 (FastAPIの例)

サーバー側でレートリミットを検知し、Retry-After を含めてレスポンスを返す実装です。

from fastapi import FastAPI, Response, status

app = FastAPI()

@app.get("/api/data")
async def get_data():
    # ここにレートリミット判定ロジックを入れる
    is_rate_limited = True 
    
    if is_rate_limited:
        # 30秒後に再試行するように通知
        return Response(
            content="Rate limit exceeded. Please try again later.",
            status_code=status.HTTP_429_TOO_MANY_REQUESTS,
            headers={"Retry-After": "30"}
        )
    return {"data": "success"}

クライアント側の挙動 (Fetch API)

クライアント側は、この Retry-After を読み取り、待機時間を計算して再試行(リトライ)をスケジュールするのが理想的です。

async function fetchWithRetry(url) {
  const response = await fetch(url);

  if (response.status === 429) {
    const retryAfter = response.headers.get("Retry-After") || 5;
    console.warn(`レート制限中。${retryAfter}秒後に再試行します...`);
    
    // 指定された時間待機してから再帰的に呼び出す
    await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
    return fetchWithRetry(url);
  }

  return response.json();
}

—

4. トラブルシューティングの現場から:注意点

この設計を導入する際、インフラエンジニアとして気をつけてほしい点がいくつかあります。

1. クライアントの「群れ」を防ぐ (Thundering Herd Problem):
もし Retry-After: 30 を返したとき、全てのクライアントが正確に「30秒後」にリトライを始めると、サーバーにスパイクが発生し、再びクラッシュします。実務では、この値にジッター(ランダムな揺らぎ)を加えるのが定石です。サーバー側で少し余裕を持たせた値を返すか、クライアント側で Retry-After の値に +α のランダムな秒数を加算して待機させるよう誘導しましょう。

2. ロードバランサーとの親和性:
AWS WAFやNginx等のリバースプロキシでレートリミットをかけている場合、それらのツールが自動的に 429 を返す設定になっているか確認してください。NGINXであれば limit_req モジュールを使うことで、非常に効率的に制御可能です。

# Nginxの設定例
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

server {
    location /api/ {
        limit_req zone=api_limit burst=5 nodelay;
        # 429を返す設定
        limit_req_status 429;
    }
}

最後に:ネットワークは「対話」である

プロトコルは、単にデータを運ぶためのルールではありません。システム同士が互いの状況を伝え合い、健全な関係を維持するための「対話の言語」です。

429 Too Many Requests と Retry-After を適切に設計することは、あなたのAPIが「独りよがりなシステム」ではなく、「クライアントを尊重し、共生を目指す成熟したシステム」であることを証明します。

もしあなたが今、API設計に悩んでいるのなら、まずはこの「拒絶の作法」から見直してみてください。それだけで、システムの信頼性は驚くほど向上するはずです。現場からは以上です。

コメント

タイトルとURLをコピーしました