【実務・中級編】 HTTPステータスコード429 (Too Many Requests)の挙動 – Web APIアーキテクチャ・データ連携実践ガイド

APIが悲鳴を上げる前に:HTTP 429 「Too Many Requests」と Retry-After が織りなす優美なレート制限の作法

ネットワークの世界に身を置いていると、何度となく「キャパシティの限界」という残酷な現実に向き合うことになります。ルーターのインターフェースがトラフィックの奔流でドロップし始めたとき、BGPピアがフラッピングを起こしたとき――そして、Web APIのバックエンドデータベースが、無慈悲なリクエストの嵐で音を上げそうになったとき。

あなたなら、APIを守るためにどうしますか?
「とりあえず接続を切る」「適当なエラーコードを返す」――そんな雑な実装をしていないでしょうか。

今回は、RFC 6585で定義された HTTPステータスコード 429 (Too Many Requests) と、それに寄り添う Retry-After ヘッダーに焦点を当てます。パケットの往来を見つめ続けてきたインフラエンジニアの視点から、クライアントを優しく、かつ毅然と押しとどめる「美しいAPI防衛術」を紐解いていきましょう。

—

なぜ「403」や「500」ではダメなのか? 429ステータスが持つ本質

APIのレート制限(Rate Limiting)を超過したクライアントに対し、どのようなレスポンスを返すべきか。歴史的背景もあり、かつては認証・認可エラーである 403 Forbidden や、単なるサーバエラーである 500 Internal Server Error、あるいは独自のカスタムコードを返す実装が散見されました。

しかし、これはクライアント側の実装者にとって悪夢でしかありません。
403 が返ってきたら「認証トークンが間違っているのか?」と勘違いしてリトライを止め、500 が返ってきたら「サーバーが壊れたんだな、数秒おきに無限リトライして復旧を待とう」と、DDoS攻撃さながらの再送ループ(Thundering Herd Problem)を引き起こすからです。

ここに光をもたらしたのが、RFC 6585で標準化された 429 Too Many Requests です。

[Client]                                    [API Gateway / Server]
   | --- (1) 過剰なリクエストの嵐 ----------> |
   |                                          | (レート制限閾値を超過)
   | <--- (2) HTTP/1.1 429 Too Many Requests - |
   |          Retry-After: 60                 |
   |                                          |
   | (3) クライアントは60秒間静かに待機       |

429ステータスコードの本質は、サーバーがこう叫んでいる点にあります。
「君の権限は正しいし、サーバーも壊れていない。ただ、今はリクエストが多すぎるんだ。少しクールダウンしてくれ」

この明確な意思表示こそが、クライアント側ロジックに「バックオフ(待機)とリトライ」という正しい振る舞いを促すトリガーとなります。

—

命綱となるヘッダー:Retry-After の正しい作法

429レスポンスを返す際、ただステータスコードを返すだけでは半人前です。行儀の良いAPIには、必ず Retry-After ヘッダーが添えられています。

Retry-After ヘッダーには、クライアントが次にリクエストを送るまで「どれだけ待つべきか」を伝えます。指定方法には主に以下の2つのパターンがあります。

1. 秒数指定(Delta-seconds)

  • 整数値で秒数を指定します。最も一般的で、プログラムによるパースが容易です。
  • 例: Retry-After: 120 (120秒=2分間待て)

2. HTTPデート指定(HTTP-date)

  • RFC 7231で定義されるGMTの絶対時刻を指定します。
  • 例: Retry-After: Wed, 21 Oct 2025 07:28:00 GMT

実務上は、ミリ秒単位の緻密な制御が必要な場合を除き、シンプルでズレの生じにくい「秒数指定」を採用するのがインフラ・バックエンド双方にとって平穏をもたらす近道です。

—

実践:Nginxによるレート制限と429の返却

では、実際のインフラ層(リバースプロキシ)でどのように429と Retry-After を構成するか、Nginxの設定例を見てみましょう。Nginxの ngx_http_limit_req_module は、この手の制御において非常に強力な武器になります。

# クライアントのIPアドレス単位で、1秒あたり平均10リクエストまでを許可するゾーンを定義
# 溢れたリクエストを一時的に溜めておくバーストサイズを20に設定
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

server {
    listen 80;
    server_name api.example.com;

    location /v1/ {
        # 定義したゾーンを適用。burstを指定し、さらにnodelayでバースト時の無駄な遅延を排除
        limit_req zone=api_limit burst=20 nodelay;
        
        # 制限値を超えた場合に返すステータスコードを明示的に429に設定(デフォルトは503)
        limit_req_status 429;

        proxy_pass http://backend_cluster;
        
        # バックエンドへのルーティング設定...
    }
}

この設定を施すことで、許容量を超えたトラフィックはNginxのレイヤーで即座に遮断され、美しく 429 Too Many Requests が返されるようになります。バックエンドのアプリケーションサーバーまで負荷を到達させない――これがインフラエンジニアとしての腕の見せどころです。

—

クライアント側の実装:429を受け入れた優美なバックオフ

サーバーが429と Retry-After を返しても、それを受け取るクライアント側が無限ループで突撃を続けていたら意味がありません。

以下に、Python(requests ライブラリ)を用いて、429と Retry-After をスマートにハンドリングする堅牢なコードの例を示します。

import time
import requests

API_URL = "https://api.example.com/v1/data"
MAX_RETRIES = 3

def fetch_data_with_rate_limit_handling():
    session = requests.Session()
    
    for attempt in range(MAX_RETRIES + 1):
        response = session.get(API_URL)
        
        # 正常レスポンスの場合
        if response.status_code == 200:
            return response.json()
            
        # 429 Too Many Requests を検知した場合
        elif response.status_code == 429:
            # Retry-Afterヘッダーから待機時間を取得(デフォルトは60秒とする)
            retry_after = int(response.headers.get("Retry-After", 60))
            
            if attempt == MAX_RETRIES:
                raise Exception("レート制限の上限に達しました。最大リトライ回数を超過したため処理を中断します。")
                
            print(f"[警告] レート制限(429)を検知しました。{retry_after}秒後にリトライします... (試行回数: {attempt + 1}/{MAX_RETRIES})")
            time.sleep(retry_after)
            continue
            
        # その他のエラーコードの場合
        else:
            response.raise_for_status()

try:
    data = fetch_data_with_rate_limit_handling()
    print("データ取得成功:", data)
except Exception as e:
    print(f"[エラー] {e}")

このコードのポイントは、サーバーから提示された Retry-After の秒数を忠実に守り、無駄なCPUサイクルやネットワーク帯域を消費せずに行儀よく待機する点です。

—

デバッグと運用の現場から:よくある罠

最後に、現場の現場で私たちが遭遇しがちな「429にまつわる罠」をいくつか共有しておきましょう。

1. プロキシやCDNによるステータスの書き換え

  • CloudflareやAWS CloudFrontなどのCDN、あるいはAPI Gatewayを経由している場合、独自のカスタム429エラーページに勝手に置き換えられることがあります。Retry-After ヘッダーが途中でドロップしていないか、curl -I などでレスポンスヘッダーを必ず実測確認してください。

2. 分散環境におけるレート制限の同期

  • 複数台のAPIサーバーで負荷分散している場合、ローカルメモリだけでカウントしていると、サーバーAではセーフでもサーバーBではアウト、といった不整合が起きます。厳密な制御が必要な場合は、Redisなどのインメモリデータストアを共有カウンターとして利用することが必須となります。

3. クライアント識別子の誤り

  • $binary_remote_addr (IPアドレス)だけで制限をかけると、社内LANやモバイルキャリアのNAT配下のユーザー全員が「一人の悪者」とみなされて巻き添え食いを起こします。認証済みAPIであれば、必ずAPIキーやJWTのユーザーIDをレート制限のキーに選定すべきです。

—

まとめ

HTTPステータスコード 429 と Retry-After は、単なるエラー通知の手段ではありません。それは、過負荷にあえぐサーバーと、データを欲するクライアントの間の「紳士協定」です。

この協定を正しく実装・運用することで、システム全体の耐障害性が飛躍的に高まり、予期せぬトラフィックの暴風雨からもインフラを守り抜くことができます。
もしあなたが今、新規のAPI設計や、頻発するエラーアラートの対応に追われているなら、今一度レスポンスのステータスコードとヘッダーを見直してみてください。プロトコルの基本に忠実であることが、最も強靭なシステムへの近道なのです。

コメント

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