429 Too Many Requestsを解決する方法:バックオフ、予算、IP分散

429は、再試行するだけでなく、レスポンスヘッダーを読むことで、どのように解決するかを正確に教えてくれる唯一のブロックです。ここでは、安全なスクレイピングスループットの背後にある算術を紹介します。

HTTP 429 Too Many Requestsは、ウェブサイトが送信できる最も正直なブロックです。403とは異なり、問題を指摘します—速すぎた—そしてしばしばレスポンスヘッダーに解決策を含めます。しかし、標準的な反応は、呼び出しを再試行ループで包み、解決可能なペーシング問題を遅くてブロックを生成する混乱に変えることです。このガイドでは、429をそのままの算術問題として扱います。レスポンスを読み、公開された制限に合わせてペースを調整し、オーバーシュートしたときに正しくバックオフし、残りの負荷をアイデンティティに分散させます。

429の意味と、それが嘘をついているとき

レートリミッターは、通常はIPアドレス、時にはAPIキーやセッションクッキーなどのアイデンティティごとにリクエストをカウントします。しきい値を超えると、コンテンツの代わりに429が返されます。実装は異なります:トークンバケットはスケジュールに従って補充される固定の許容量を提供し、スライディングウィンドウは時計の分ではなくローリング期間でカウントし、階層化システムはより高いハードブロックでソフトリミットでスロットルします。どれに直面するかは、短い休止が十分か、ウィンドウ全体を待たなければならないかを決定します。

ここで、数時間を節約する警告:最初のリクエストでの429はレート制限ではありません。それはレート制限のコスチュームを着たボットの応答です。広く読まれているStack Overflowのスレッドはこれを正確に説明しています—スクレイパーの最初の呼び出しが「Misbehaving Content Scraper Please use robots.txt Your IP has been rate limited」と読むページとともに429を返しました。何も超えていませんでした。サーバーは単にクライアントをボットと判断し、そのコードを選びました。何らかのボリュームを送信する前に429を見た場合、それを検出問題として扱い、403 forbidden errorsに関するガイドでヘッダーとIPチェックを行ってください。

コードを変更する前にレスポンスを読んでください

行儀の良いサーバーは、いつ戻ってくるべきかを教えてくれます。Retry-Afterは秒数またはHTTP日付を持ちます。多くのAPIはX-RateLimit-Limit(上限)、X-RateLimit-Remaining(現在のウィンドウでの残り)およびX-RateLimit-Reset(再補充の時)を追加します。これら3つは、反応的な再試行をプロアクティブなペーシングに変えます:ブロックの前に速度を落とすことができます。

import requests

r = requests.get("https://target.example/api/items", timeout=20)
print(r.status_code)
for h in ("Retry-After", "X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset"):
    if h in r.headers:
        print(f"{h}: {r.headers[h]}")

# No headers at all? The limit is undocumented - measure it:
# send a slow ramp (1 req/s, then 2, then 4) and note where 429 starts.

役立つものが何も返ってこない場合は、リミットを自分で計測してください:1秒あたり1リクエストで1分間実行し、次に2つ、次に4つとし、429が現れる速度を記録します。10分の測定は1週間の推測に勝ります。そして見つけた数値が、他のすべてが構築される予算になります。

100リクエスト毎分のレート制限に対する安全、限界、429トリガーリクエストギャップを示すバンド図
全計算:60秒を公開された制限で割り、ネットワークジッターのバッファを追加します。

ペーシングの算術

文書化された制限を取り、割ります。毎分100リクエストの上限は、60 / 100 = 0.6秒のリクエスト間隔を絶対的な下限とします—そして下限は目標ではありません。ネットワーク遅延は変動し、あなたの時計とサーバーの時計は一致せず、ウィンドウ境界でのバーストは見かけの速度を倍増させることがあります。制限の70-80%を目指してください:その例では約0.8秒のリクエスト間隔で、毎分75ページをまだ提供します。

同時実行は同じ数から導き出されます。1秒あたり1.25リクエストを希望し、各リクエストがラウンドトリップで2秒かかる場合、1.25 x 2 = 2.5のインフライトリクエストが必要です—したがって、セマフォは3であり、非同期コードのデフォルトの50ではありません。制限なしの非同期ファンアウトは、429の最も一般的な原因です:一度に100のコルーチンが起動されると、どれだけ礼儀正しく見えても、瞬間的なバーストとして到着します。asyncioでスクレイピングする場合、httpxとaiohttpを使用したPython非同期スクレイピングのセマフォパターンが解決策です。

機能するバックオフ:指数関数的、上限付き、ジッター付き

429に遭遇した場合、Retry-Afterが存在する場合はそれを尊重してください。そうでない場合は1秒から始め、倍増します—1、2、4、8、16—硬い上限まで、壊れたターゲットがキューを永遠に停止させないようにします。そしてジッターを追加します。ランダム化がなければ、同じ瞬間に壁にぶつかったすべてのワーカーが同じ瞬間に再試行し、問題を引き起こしたバーストを再現します。

import random, time, requests

def get_with_backoff(session, url, max_tries=6, cap=120.0):
    for attempt in range(max_tries):
        r = session.get(url, timeout=20)
        if r.status_code != 429:
            return r

        ra = r.headers.get("Retry-After", "")
        wait = float(ra) if ra.isdigit() else 2.0 ** attempt   # 1, 2, 4, 8, 16, 32
        wait = min(wait, cap)
        wait += random.uniform(0, wait * 0.3)                   # jitter: break the lockstep

        time.sleep(wait)
    raise RuntimeError(f"still 429 after {max_tries} attempts: {url}")

さらに良いのは、ループを閉じることです。加算増加/乗算減少は、限界を自動的に見つけてその直下に留まるスクレイパーを提供します:レスポンスがクリーンな間は速度を上げ、429が着地した瞬間に半分にします。ドメインごとに1つのペーサーを維持してください—制限はホストごとであり、1つの攻撃的なターゲットが他の40を遅くするべきではありません。

class Pacer:
    """One per domain. Additive increase, multiplicative decrease."""
    def __init__(self, rps=2.0, floor=0.2, ceiling=8.0):
        self.rps, self.floor, self.ceiling = rps, floor, ceiling

    def ok(self):          # clean response: creep faster
        self.rps = min(self.ceiling, self.rps + 0.05)

    def throttled(self):   # 429: halve immediately
        self.rps = max(self.floor, self.rps / 2)

    @property
    def gap(self):
        return 1.0 / self.rps

負荷の分散:予算はプロジェクトごとではなくアイデンティティごと

ペーシングが正しく行われていても、さらにスループットが必要な場合、残された唯一のレバーはアイデンティティです。カウンターがIPにキー付けされているため、Nの出口IPはN倍の予算を提供します—算術はそれほど単純です。サイトがアドレスごとに毎分60リクエストを許容し、毎分1,200ページが必要な場合、それはキャップを大幅に下回る20の同時出口が必要であり、1つの出口がその20倍を超えて実行されるのではありません。

これがローテーティングプロキシの実際の目的です。ローテーティングゲートウェイは、90M以上のアドレスプールから200以上の国にわたる異なる住宅IPを各リクエストに割り当てるため、IPごとのカウンターは決して満たされません。負荷の分散とプールの燃焼の違いを生む2つのルール:ローテーション後でもIPごとのレートを制限以下に保つ(ローテーションは予算を増やしますが、削除しません)、複数のリクエストにまたがるフローにはスティッキーセッションを使用する—ログイン、カート、ページネーションされた結果セット—セッションが途中で切れないようにします。数分間同じIPが必要で、その後新しいものが必要な場合、トレードオフはスティッキーセッション対ローテーティングプロキシで説明されています。

住宅IPでレート予算を増やす

HTTP 429エラーを回避するための主要な数値を示す統計パネル:600msの最小ギャップ、指数関数的バックオフ、50%のレートカット、IPごとの予算
4つの数値がシステム全体を運営します。それらをターゲットの制限から導き出し、楽観主義からは導き出さないでください。

より多くのIPよりも安価:リクエストを減らす

帯域幅の節約は二重の利益をもたらします—リクエストが少ないほど429が少なく、請求も少なくなります。これは、プロキシ帯域幅コスト削減で私たちが主張するのと同じ議論です。そして、ペーシングインフラストラクチャを全く構築したくない場合、QuantumProxiesのScraper APIは、再試行、ローテーション、ドメインごとのスロットルを1つのエンドポイントの背後で吸収し、markdown、JSON、HTMLを返します。

よくある質問

PythonでHTTP error 429 too many requestsを回避するにはどうすればよいですか?

ターゲットの公開された制限に基づいてリクエスト間に意図的なギャップを設定し、レートと遅延に合わせたセマフォで同時実行を制限し、Retry-Afterが現れた場合はそれを尊重し、指数関数的バックオフとジッターを加えた再試行を行います。それでもさらにスループットが必要な場合は、ギャップを短くするのではなく、ローテーティングプロキシIPにリクエストを分散させます。

429の後、どのくらい待つべきですか?

サーバーがそれを送信する場合、Retry-Afterが示す通りに正確に待ちます—それは秒数またはHTTP日付である可能性があります。そのヘッダーがない場合は、1秒から始め、次の429ごとに倍増し、1分または2分の上限まで増やし、並行ワーカーが一斉に再試行しないようにランダムジッターを追加します。

プロキシは429エラーを修正しますか?

予算を増やしますが、制限を取り除くわけではありません。カウンターはクライアントIPにキー付けされているため、多くの住宅出口にわたってランを分散させることで、各アドレスをしきい値以下に保ちます。しかし、IPごとの制限を10倍超えて叩かれるプールは、依然として429を集め、その評判を損ないます。まずペースを整え、それからローテーションします。

429はバンと同じですか?

いいえ。429は設計上一時的であり、ウィンドウがリセットされるとクリアされます。これは403のアイデンティティブロックと区別される点です。それを繰り返し無視することが、持続的なオーバーシュートがスロットルを長期間のIPバンに昇格させる正確なシグナルです。

なぜ最初のリクエストで429を受け取るのですか?

実際には何もカウントされていなかったからです。一部のサーバーは、ボリュームに関係なく、ボットと見なすクライアントに429を返します—ステータスコードは単に彼らの選んだ応答です。レスポンスボディを確認してください:robots.txt、スクレイパー、またはファイアウォールに言及している場合は、ペーシングではなく、ヘッダー、TLSフィンガープリント、出口IPを修正してください。

レート制限を意図的に使う予算として扱ってください。上限を測定し、その70-80%で実行し、オーバーシュートしたときにジッターでバックオフし、ペーシングが正しいときにのみアイデンティティを増やします。その順序で行えば、429はエラークラスではなく、設定ファイルの数値になります。

ローテーティングプロキシを入手し、レート制限との戦いをやめましょう