サーバーが悲鳴を上げる前に:HTTP 429 Too Many RequestsとRetry-Afterの正しい作法
インフラエンジニアとして現場を渡り歩いていると、「APIが繋がらない」というアラートほど心臓に悪いものはありません。特に、クライアントからの無慈悲なリクエストの波状攻撃によって、バックエンドのデータベースが悲鳴を上げている状況は、まさに地獄絵図です。
そんな時、設計者として「ただエラーを返す」のではなく、「どうすれば健全に回復できるか」をクライアントに伝えることこそが、優れたWeb APIの条件です。今日は、RFC 6585で定義された 429 Too Many Requests と、その相棒である Retry-After ヘッダーについて、現場の知見を交えて深く掘り下げていきます。
—
1. なぜ「429」なのか?:503 Service Unavailableとの境界線
初心者がやりがちなミスとして、レート制限超過時に 503 Service Unavailable を返してしまうケースがあります。しかし、これはプロトコル的には誤りです。
- 503 Service Unavailable: サーバーが過負荷やメンテナンスで「一時的に処理できない」状態。クライアントに非はない。
- 429 Too Many Requests: クライアントが「許容範囲を超えてリクエストを送ってきた」状態。クライアント側に「少し自重してくれ」と伝えるためのステータスコードです。
この違いは重要です。負荷分散装置(ロードバランサー)やCDNの設定で、「429なら制限を強める」「503ならバックエンドの死活監視を行う」といった切り分けをするためにも、セマンティクスを正しく守ることは運用コストを劇的に下げます。
—
2. 賢い再試行を促す Retry-After ヘッダーの流儀
ただ「やりすぎだ」と拒絶するだけでは、クライアントは短時間のループで再試行を繰り返し、ネットワークを無駄に占有する「DDoSじみた挙動」を引き起こします。ここで登場するのが Retry-After ヘッダーです。
このヘッダーには、以下のいずれかを指定します。
1. 秒数 (HTTP-date または Delta-seconds): 「何秒待てばいいか」という整数値。
2. 絶対時刻 (HTTP-date): 「この時刻になれば再開してよい」という日付形式。
圧倒的におすすめなのは「秒数」です。クライアントとサーバーの時計が同期していないリスクを回避できるからです。
—
3. 実践:クライアント側の再試行ロジック
もしあなたがAPIクライアントを実装する側なら、以下のようなロジックを組み込むのが「大人の作法」です。単にリトライするのではなく、Retry-After を尊重しましょう。
Pythonによる実装例
import requests
import time
def call_api_with_retry(url):
response = requests.get(url)
# 429が返ってきた場合
if response.status_code == 429:
# Retry-Afterヘッダーを取得(デフォルトは60秒待機)
wait_time = int(response.headers.get("Retry-After", 60))
print(f"制限超過です。{wait_time}秒待機します...")
time.sleep(wait_time)
# 再帰的に再試行
return call_api_with_retry(url)
return response.json()
—
4. インフラ側での制御:Nginxによるレート制限
インフラスペシャリストとして、アプリケーションに負荷をかけずにフロントゲートウェイでこの制限をかけるのが最も効率的です。Nginxを例に挙げましょう。
Nginx設定ファイル (nginx.conf)
# IPアドレス単位でリクエストを制限する設定
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=5r/s;
server {
location /api/ {
# 5リクエスト/秒を超えたら429を返す
# nodelayを指定しないと、バーストしたリクエストはキューに入れられ、
# 処理が遅延する可能性があります。即座に429を返すには必須です。
limit_req zone=api_limit burst=10 nodelay;
# 429エラーをカスタマイズ(必要に応じて)
error_page 429 = @rate_limited;
}
location @rate_limited {
add_header Retry-After 30 always; # 30秒待機させる
return 429 '{"error": "Too Many Requests", "retry_after": 30}';
}
}
—
5. 現場のシニアからのアドバイス:指数バックオフの重要性
最後に一つだけ、現場でよくある失敗談を。
Retry-After を守ることは基本ですが、もしサーバー側が何らかの理由でヘッダーを返せなかった場合、クライアントが即座にリトライを繰り返すと「自爆リトライ」の嵐が始まります。
必ず「指数バックオフ(Exponential Backoff)」を実装してください。
リトライするたびに「待機時間を2倍、4倍、8倍…」と増やし、さらにランダムな「ジッター(揺らぎ)」を加えることで、リクエストの集中を緩和できます。
- 1回目失敗: 1秒待機
- 2回目失敗: 2秒待機 + ジッター
- 3回目失敗: 4秒待機 + ジッター
ネットワークというものは、常に不安定で、予測不可能です。だからこそ、APIの設計には「相手を信じすぎない」という謙虚な精神が必要なのです。
さあ、皆さんもセマンティクスを正しく守った、美しいAPI設計を心がけてください。それが結果として、あなたの深夜の呼び出し回数を減らす唯一の道なのですから。
コメント