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

ネットワークの世界へようこそ。今日もパケットの荒波を乗りこなしているだろうか。

REST APIを設計する際、我々エンジニアはどうしても「正常系」の美しさに目を奪われがちだ。いかにリソースをクリーンに表現するか、どのメソッドが適切か。しかし、真に堅牢で「美しい」システムを分かつのは、実は「異常系」、それもトラフィックが臨界点を超えた時の振る舞いにある。

今回は、APIの過負荷や乱用からシステムを守る最後の砦、HTTP 429 Too Many Requestsと、その相棒であるRetry-Afterヘッダーについて深掘りしていこう。

—

1. 429 Too Many Requests:サーバーからの「丁寧な拒絶」

かつて、レートリミット(流量制限)を超えた際のレスポンスには 503 Service Unavailable が流用されることが多かった。しかし、503は「サーバーが一時的にダウンしている」というニュアンスが強く、原因がサーバー側にあるのかクライアント側にあるのかが判然としなかった。

そこで登場したのが、RFC 6585 で定義された 429 Too Many Requests だ。

このステータスコードの本質は、「あなたのリクエストは正当だが、送るペースが早すぎる。少し落ち着いてくれ」という、クライアントに対する明確なフィードバックにある。これを適切に実装することで、クライアント側のライブラリは「故障」ではなく「待機」が必要であることを正しく理解できる。

—

2. Retry-After ヘッダー:再試行への道標

429を返す際、セットで送るべきなのが Retry-After ヘッダーだ。これがない429は、出口のない迷路のようなものだ。クライアントは「いつ再開していいのか」が分からず、闇雲にリトライを繰り返し、結果としてサーバーの首を絞め続けることになる。

Retry-After には、大きく分けて2つの指定方法がある。

A. 遅延秒数による指定(相対時間)

最も一般的で扱いやすい方法だ。何秒後にリトライすべきかを整数で示す。

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 30

{
    "error": "Rate limit exceeded. Please try again in 30 seconds."
}

B. 日時による指定(絶対時間)

HTTP-date形式で、再試行可能な時刻を明示する。

HTTP/1.1 429 Too Many Requests
Retry-After: Wed, 21 Oct 2023 07:28:00 GMT

実務においては、サーバーとクライアント間の時刻同期(NTP)のズレを考慮する必要がない「秒数指定」の方がトラブルが少なく、推奨されることが多い。

—

3. 通信シーケンス:レートリミット発生の瞬間

現場で何が起きているのか、シーケンス図で整理してみよう。バースト的なアクセスが発生した際の挙動だ。

1. Client: GET /api/v1/resource (101回目のリクエスト)
2. Server: レートリミット(上限100)を確認
3. Server: 429 Too Many Requests を発行

  • Retry-After: 10 を付与

4. Client: レスポンスを解析。10秒間のスリープに入る
5. Client: 10秒経過後、リクエストを再送
6. Server: 制限が解除されていれば 200 OK を返却

この「10秒間のスリープ」をクライアントが自律的に行うことで、インフラ全体のトポロジーが安定する。

—

4. インフラ層での実装例(Nginx)

APIアプリケーション側でカウントを実装するのも手だが、パフォーマンスを考えるならリバースプロキシ層で食い止めるのが鉄則だ。Nginxでの設定例を見てみよう。

# ゾーンの定義:10MBの共有メモリを使い、IPアドレスごとに毎秒5リクエストを許可
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=5r/s;

server {
    location /api/ {
        # 定義したゾーンを適用。一時的なバーストは5リクエストまで許容
        limit_req zone=api_limit burst=5 nodelay;

        # 制限超過時のステータスコードを429に設定(デフォルトは503)
        limit_req_status 429;
        
        # ※Nginx標準ではRetry-Afterを自動付与しないため、
        # 必要に応じてmapやLuaモジュールでヘッダーを追加する設計が望ましい
    }
}

—

5. クライアント側の実装:賢いリトライ戦略

「429が返ってきたから即座にループでリトライ」は最悪の悪手だ。それは単なるDoS攻撃と変わらない。プロフェッショナルなクライアント実装には、「Exponential Backoff(指数関数的退避)」と「Jitter(ゆらぎ)」を取り入れるべきだ。

以下はPythonを用いた、Retry-After を尊重する堅牢なリトライ処理の例だ。

import requests
import time

def call_api_with_retry(url):
    max_retries = 5
    for attempt in range(max_retries):
        response = requests.get(url)

        if response.status_code == 200:
            return response.json()

        if response.status_code == 429:
            # Retry-Afterヘッダーを取得(存在しない場合は指数バックオフ)
            retry_after = response.headers.get("Retry-After")
            
            if retry_after:
                wait_time = int(retry_after)
            else:
                # 指数バックオフ: 2^attempt + ゆらぎ
                wait_time = (2 ** attempt) + (time.time() % 1)
            
            print(f"Rate limited. Waiting for {wait_time}s...")
            time.sleep(wait_time)
            continue
        
        # その他のエラー処理
        response.raise_for_status()

    raise Exception("Max retries exceeded")

JavaScript (Fetch API) の場合も同様のロジックが必要になる。

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

  if (response.status === 429) {
    // ヘッダーから待ち時間を取得
    const retryAfter = response.headers.get('Retry-After');
    const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1000 : 2000;

    console.warn(`429 Hit. Retrying after ${waitTime}ms`);
    
    // 指定時間待機して再帰的に実行
    await new Promise(resolve => setTimeout(resolve, waitTime));
    return fetchWithRetry(url);
  }

  return response.json();
}

—

6. 実務でのアドバイス:美しさは「優しさ」に宿る

APIの設計者として、429を返すことは「拒絶」ではなく「システムの持続可能性のための対話」だと考えてほしい。

1. ドキュメントに明記せよ: どのエンドポイントにどのようなレートリミットがあるか、開発者ガイドに記載すること。
2. X-RateLimit ヘッダーを検討せよ: RFC標準ではないが、X-RateLimit-Limit(上限)や X-RateLimit-Remaining(残り回数)を返すことで、クライアントは429に陥る前に自制することができる。
3. 監視を怠るな: 特定のクライアントが常に429を叩いているなら、それは設計ミスか、より上位のプラン(あるいはキャパシティ)への移行が必要なサインだ。

ネットワークプロトコルの深淵において、429 と Retry-After は、サーバーとクライアントが互いを尊重し合うための「礼儀作法」である。この作法をマスターした時、君の設計するAPIは、ただ動くだけの道具から、過酷なインターネット環境を生き抜く強靭なエコシステムへと昇華するはずだ。

パケットの向こう側にいるエンジニアが、君の返した Retry-After を見て「助かる」と呟く。そんな設計を目指してみてはどうだろうか。

コメント

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