핵심 요약: HTTP 429 에러는 대상 API 또는 WAF의 요청 속도 제한(Rate Limit)을 나타내며, 디코릴레이티드 지터(Decorrelated Jitter) 백오프와 순환 주거용 프록시 풀로 해결합니다. Cloudflare 520 에러("Web Server Returned an Unknown Error")는 크롤러의 과부하로 인해 원본 서버에서 프로세스 다운, TCP RST 패킷 전송, 과도한 헤더가 발생할 때 발생하며, Keep-Alive 튜닝과 서킷 브레이커로 방어합니다.
1. 도입: 자율형 AI 웹 크롤러의 취약점
자율형 AI 에이전트가 단순 대화형 인터페이스를 넘어 다단계 추론 엔진(실시간 웹 리서치, 금융 실사, 경쟁사 모니터링, 코드 검색)으로 발전하면서, 핵심 병목 구간은 LLM의 처리 속도가 아닌 네트워크 신뢰성과 엣지 접근성이 되었습니다.
최신 프로덕션 에이전트(LangGraph, Eve, OpenClaw 기반)는 분당 수천 건의 HTTP 호출을 전송합니다. 기존 웹 크롤러(Scrapy, Googlebot)와 달리 AI 에이전트는 다음을 필수로 요구합니다:
- 초저지연 동기식 웹 수집 (추론 루프 중단 방지).
- 정교한 JavaScript 실행 (SPA 및 동적 하이드레이션 DOM 처리).
- 토큰 낭비를 최소화하는 깨끗한 Markdown 변환.
그러나 Cloudflare, Akamai, DataDome 등 최신 엣지 인프라는 정교한 봇 탐지 휴리스틱을 작동시킵니다. 요청 동시성이나 TLS 핸드셰이크 설정이 어긋나면 두 가지 치명적 에러가 시스템을 마비시킵니다:
- HTTP 429 Too Many Requests: 클라이언트가 지정된 시간 내에 허용된 요청 수를 초과함.
- HTTP 520 Web Server Returned an Unknown Error: 크롤러의 집중 요청으로 인해 원본 웹 서버가 비정상 응답을 반환할 때 Cloudflare가 생성하는 5xx 상태 코드.
2. HTTP 429의 해부: 속도 제한, 토큰 버킷, TLS 핑거프린팅
RFC 6585에 정의된 429 에러는 AI 수집 시 세 가지 계층에서 발생합니다:
- 계층 1: 엣지 WAF (Cloudflare / DataDome): Python OpenSSL의 JA4 핑거프린트와 요청 헤더의 Chrome User-Agent 간 불일치를 감지하여 즉각 차단 또는 429 반환.
- 계층 2: API 게이트웨이 (Kong / Envoy / Nginx): 토큰 버킷(Token Bucket) 및 리키 버킷(Leaky Bucket) 알고리즘 적용.
- 계층 3: 원본 서버 애플리케이션: DB 커넥션 풀 고갈 및 CPU 보호 훅 작동.
필수 분석 헤더: Retry-After, RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset.
3. Cloudflare 520 에러의 원인
520 에러는 원본 서버가 Cloudflare 엣지 프록시가 해석할 수 없는 응답을 반환했음을 의미합니다:
- 백엔드 프로세스 다운 (OOM / SIGSEGV): 크롤러의 동시 접속 폭증으로 원본 서버 메모리가 고갈되어 프로세스가 종료되고 TCP
RST패킷이 Cloudflare로 전송됨. - Keep-Alive 타임아웃 불일치: Cloudflare는 기본 15초 동안 커넥션을 유지합니다. 원본 Nginx의
keepalive_timeout이 5초로 설정되어 있으면 요청 전송 도중 소켓이 끊어져 520이 발생합니다. - 응답 헤더 버퍼 초과 (>16KB): 과도한 Set-Cookie 루프로 인해 엣지 버퍼 한계 초과.
- 빈 응답 (Zero Bytes): TLS 협상 후 원본 서버가 아무런 바이트도 전송하지 않고 연결을 종료.
4. 알고리즘적 해결책: Decorrelated Jitter 및 서킷 브레이커
단순 재시도 루프(sleep(1))는 군집 현상(Thundering Herd Problem)을 유발합니다. 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"서킷 브레이커 차단 작동 중: {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 JA4 핑거프린트 우회 (curl_cffi)
표준 라이브러리 사용 시 발생하는 봇 차단을 우회하기 위해 Chrome과 동일한 TLS 암호화 스위트 및 HTTP/2 프레임을 생성하는 curl_cffi를 도입해야 합니다:
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. 프록시 전략 및 서버측 520 방지 설정
- 순환형 주거용 프록시 (Rotating Residential): 수백만 개의 실제 가정용 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대 핵심 전략:
- Decorrelated Jitter로 재시도 요청 폭증 방지.
curl_cffi를 통한 완벽한 브라우저 TLS JA4 모방.- 원본 웹 서버의 Keep-Alive 설정을 75초 이상으로 동기화.
- 서킷 브레이커를 통한 무의미한 LLM 토큰 낭비 차단.