Webスクレイピング & エージェント

AIクローラー・エージェントにおけるHTTP 429およびCloudflare 520エラーの完全解決策

要約: HTTP 429エラーはAPIやWAFのレート制限超過を示し、無相関ジッター(Decorrelated Jitter)バックオフ、トークンバケットアルゴリズム、ローテーション型レジデンシャルプロキシによって解決できます。Cloudflare 520エラー("Web Server Returned an Unknown Error")は、高負荷スクレイピング時にオリジンサーバーのプロセス停止、TCP RSTリセット、巨大ヘッダーにより発生します。HTTP/2 Keep-Aliveの調整とサーキットブレーカーで防止します。

1. はじめに:自律型AIクローラーの脆弱性と課題

自律型AIエージェントが単純なチャットボットから、多段階推論を行うエンジン(リアルタイムWeb調査、金融デューデリジェンス、自動競合分析、高頻度コード検索)へと進化する中で、システムの主要なボトルネックはLLMの推論速度ではなく、ネットワークの堅牢性とエッジ環境へのアクセス性へとシフトしました。

Deep Research swarmやLangGraph、OpenClawなどの最新エージェントは、1分間に数千件ものHTTPリクエストを送信します。従来の検索エンジン用クローラー(Scrapy、Googlebot)とは異なり、AIエージェントには以下の要件があります:

  1. 低遅延かつ同期的なWeb取得(推論ループをブロックしない)。
  2. 高度なJavaScript実行(SPAや動的ハイドレーションDOMの解析)。
  3. 不要トークンを削ぎ落としたクリーンなMarkdown変換

しかし、Cloudflare、Akamai、DataDomeなどのエッジ保護環境は高度なアンチボット検知を展開しています。並行数やTLSハンドシェイクの設定を誤ると、以下の2大エラーによってクローラーが停止します:

  • HTTP 429 Too Many Requests: クライアントが一定時間内のレート制限を超過した状態。
  • HTTP 520 Web Server Returned an Unknown Error: Cloudflare専用の5xxエラーコード。オリジンサーバーが高負荷で異常終了したり、TCPコネクションを強制切断した際に発生。

2. HTTP 429の構造:レート制限、トークンバケット、TLSフィンガープリント

RFC 6585で定義される429エラーは、AIクローラーでは以下の3層で発生します:

  1. 第1層:エッジCDN / WAF(Cloudflare / DataDome): Pythonの標準ライブラリ(OpenSSL)のJA4フィンガープリントと送信されたChromeのUser-Agentの不一致を検知し、403の代わりに429やCAPTCHAを返送。
  2. 第2層:APIゲートウェイ(Kong / Envoy / Nginx): トークンバケット(Token Bucket)やリーキーバケット(Leaky Bucket)による数学的流量制御。
  3. 第3層:オリジンWebサーバー: DBコネクション枯渇やサーバー過負荷保護による制限。

解析すべきレスポンスヘッダー:Retry-AfterRateLimit-LimitRateLimit-RemainingRateLimit-Reset


3. Cloudflare エラー 520 のメカニズム

エラー520は、オリジンサーバーがCloudflareエッジに対して無効または予期しないレスポンスを返した際に生成されます:

  1. オリジンプロセスのクラッシュ(OOM / SIGSEGV): クローラーの急激な並行アクセスでメモリが枯渇し、プロセスが強制終了してTCP RSTがCloudflareに送信される。
  2. Keep-Aliveタイムアウトの不一致: Cloudflareのエッジはデフォルトで15秒間コネクションを保持します。オリジンのNginxが5秒で切断する場合、通信の瞬断で520が発生。
  3. ヘッダーサイズのオーバーフロー(>16KB): 膨大なSet-Cookieループがエッジの上限を超過。
  4. ゼロバイト応答: TLS確立後にサーバーがデータを送信せずソケットを切断。

4. アルゴリズム的対策:Decorrelated Jitterとサーキットブレーカー

単純な待機ループ(sleep(1))は群れ現象(Thundering Herd)を引き起こし、障害を悪化させます。AWSが提唱するDecorrelated Jitter(無相関ジッター)が最適な待機時間を生成します:

# Decorrelated Jitter 式:
sleep = min(cap, random.uniform(base, previous_delay * 3.0))

Python実装例(サーキットブレーカー搭載)

import asyncio, random, time, aiohttp
from urllib.parse import urlparse

class ResilientAgentCrawler:
    def __init__(self, base_delay=1.0, max_delay=60.0, max_retries=5, threshold=4, cooldown=30.0):
        self.base_delay = base_delay
        self.max_delay = max_delay
        self.max_retries = max_retries
        self.threshold = threshold
        self.cooldown = cooldown
        self.failures = {}
        self.opened_at = {}

    def _is_open(self, domain):
        t = self.opened_at.get(domain)
        if not t: return False
        if time.monotonic() - t > self.cooldown:
            del self.opened_at[domain]
            self.failures[domain] = 0
            return False
        return True

    async def fetch(self, session, url):
        domain = urlparse(url).netloc
        if self._is_open(domain):
            raise RuntimeError(f"Circuit Breaker 作動中: {domain}")

        delay = self.base_delay
        for attempt in range(1, self.max_retries + 1):
            try:
                async with session.get(url) as resp:
                    if resp.status == 200:
                        self.failures[domain] = 0
                        return await resp.text()
                    elif resp.status == 429:
                        retry_after = resp.headers.get("Retry-After")
                        wait = float(retry_after) if retry_after else random.uniform(self.base_delay, delay * 3.0)
                        delay = min(self.max_delay, wait)
                        await asyncio.sleep(delay)
                    elif resp.status in (520, 502, 503, 504):
                        self.failures[domain] = self.failures.get(domain, 0) + 1
                        if self.failures[domain] >= self.threshold:
                            self.opened_at[domain] = time.monotonic()
                        delay = min(self.max_delay, random.uniform(self.base_delay, delay * 3.0))
                        await asyncio.sleep(delay)
                    else:
                        resp.raise_for_status()
            except Exception as e:
                if attempt == self.max_retries: raise e
                await asyncio.sleep(delay)

5. TLSフィンガープリント偽装(curl_cffi

2026年においてUser-Agentの偽装だけでは不十分です。curl_cffiを使用し、JA4フィンガープリントとHTTP/2フレームをChromeに完全一致させます:

from curl_cffi.requests import AsyncSession

async def fetch_stealth(url: str):
    async with AsyncSession(impersonate="chrome124") as s:
        res = await s.get(url, timeout=15)
        return res.text

6. プロキシ選定とサーバー側の防御

  • ローテーション型レジデンシャルプロキシ: 数百万の家庭用IPに分散し、429制限を回避。
  • モバイルプロキシ(4G/5G・CGNAT): 数万人の一般ユーザーと同一IPを共有するため、WAFの制限値が大幅に緩和される。
  • オリジンサーバー(Nginx)の520防止設定:
http {
    keepalive_timeout 75s;
    keepalive_requests 10000;
    proxy_buffer_size 128k;
    proxy_buffers 4 256k;
}

7. まとめ

AIエージェントのクローラーを安定稼働させるための4原則:

  1. 単純な待機を廃止し、Decorrelated Jitterを導入する。
  2. curl_cffiでブラウザのTLS JA4署名を模倣する。
  3. オリジンサーバーのKeep-Aliveを75秒以上に設定する。
  4. サーキットブレーカーによりLLMトークンの浪費を防ぐ。
← 記事一覧へ
0 / 4