429 Too Many Requests 오류 해결: 백오프, 예산 및 IP 분산
429는 문제를 해결하는 방법을 정확히 알려주는 유일한 차단입니다 — 단순히 재시도하는 대신 응답 헤더를 읽으면 됩니다. 안전한 스크래핑 처리량의 산술적 계산을 소개합니다.
HTTP 429 Too Many Requests는 웹사이트가 보낼 수 있는 가장 솔직한 차단입니다. 403과 달리 문제를 명확히 지적하며 — 너무 빠르게 요청했다는 것 — 종종 해결책을 응답 헤더에 포함합니다. 그러나 일반적인 반응은 호출을 재시도 루프에 감싸고 희망을 갖는 것입니다. 이는 해결 가능한 속도 문제를 느리고 차단을 발생시키는 혼란으로 바꿉니다. 이 가이드는 429를 수학적 문제로 다룹니다: 응답을 읽고, 게시된 제한에 맞춰 속도를 조절하고, 초과 시 올바르게 백오프하며, 남은 부하를 여러 신원에 분산하세요.
429의 의미와 언제 거짓말을 하는지
속도 제한기는 신원당 요청을 계산합니다 — 보통 IP 주소, 때로는 API 키나 세션 쿠키 — 시간 창 내에서. 임계치를 넘으면 콘텐츠 대신 429를 받습니다. 구현은 다릅니다: 토큰 버킷은 일정에 따라 다시 채워지는 고정 할당량을 제공하고, 슬라이딩 윈도우는 시계 분이 아닌 롤링 기간 동안 계산하며, 계층 시스템은 높은 곳에서 강력하게 차단하기 전에 부드러운 제한에서 조절합니다. 어떤 것을 마주하느냐에 따라 짧은 일시 중지가 충분한지 아니면 전체 창을 기다려야 하는지가 결정됩니다.
이제 시간을 절약하는 주의사항: 첫 번째 요청에서 429가 발생하면 속도 제한이 아닙니다. 이는 속도 제한 코스튬을 입은 봇 응답입니다. 널리 읽히는 Stack Overflow 스레드는 정확히 이를 설명합니다 — 스크래퍼의 첫 번째 호출이 "잘못된 콘텐츠 스크래퍼 robots.txt를 사용하세요. 귀하의 IP가 속도 제한되었습니다"라는 페이지와 함께 429를 반환했습니다. 아무것도 초과되지 않았고, 서버는 단순히 클라이언트를 봇으로 판단하고 해당 코드를 선택했습니다. 볼륨을 보내기 전에 429를 본다면, 이는 탐지 문제로 취급하고 403 금지 오류에 대한 가이드에서 헤더와 IP 검사를 수행하세요.
코드를 변경하기 전에 응답을 읽으세요
잘 동작하는 서버는 언제 돌아와야 하는지 알려줍니다. Retry-After는 초 수 또는 HTTP 날짜를 포함합니다. 많은 API는 X-RateLimit-Limit (상한), X-RateLimit-Remaining (현재 창에서 남은 양) 및 X-RateLimit-Reset (다시 채워지는 시점)을 추가합니다. 이 세 가지는 반응적인 재시도를 능동적인 속도 조절로 바꿉니다: 차단 후가 아닌 전에 속도를 줄일 수 있습니다.
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초에 한 번씩 요청을 보내고, 그 후 2, 4로 늘리며 429가 나타나는 비율을 기록하세요. 10분의 측정이 일주일의 추측보다 낫고, 발견한 숫자가 모든 것의 예산이 됩니다.

속도 조절 산술
문서화된 제한을 나누세요. 분당 100 요청의 상한은 60 / 100 = 0.6초 요청 간격을 절대적인 최저로 의미합니다 — 그리고 최저는 목표가 아닙니다. 네트워크 지연은 변동하고, 귀하의 시계와 서버의 시계는 일치하지 않으며, 창 경계에서의 폭발은 귀하의 명백한 비율을 두 배로 만들 수 있습니다. 제한의 70-80%를 목표로 하세요: 그 예에서는 요청당 약 0.8초로, 여전히 분당 75페이지를 제공합니다.
동시성은 같은 숫자에서 파생됩니다. 초당 1.25 요청을 원하고 각 요청이 왕복 2초 걸린다면, 1.25 x 2 = 2.5의 비행 요청이 필요합니다 — 따라서 비동기 코드의 기본값인 50이 아닌 세마포어 3입니다. 제한되지 않은 비동기 팬아웃은 429의 가장 일반적인 원인입니다: 동시에 시작된 백 개의 코루틴은 아무리 평균이 공손해 보여도 하나의 순간적인 폭발로 도착합니다. 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가 발생하는 즉시 절반으로 줄입니다. 도메인당 하나의 속도 조절기를 유지하세요 — 제한은 호스트당이며, 하나의 공격적인 대상이 나머지 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개의 동시 출구이지, 20배 초과하여 실행되는 하나의 출구가 아닙니다.
이것이 회전 프록시의 실제 용도입니다. 회전 게이트웨이는 200개 이상의 국가에 걸쳐 90M+ 주소 풀에서 각 요청에 다른 주거용 IP를 제공합니다, 그래서 IP당 카운터가 절대 채워지지 않습니다. 부하를 분산시키고 풀을 소모하는 것의 차이를 만드는 두 가지 규칙: 회전 후에도 IP당 속도를 제한 아래로 유지하세요 (회전은 예산을 곱하지만 제거하지 않습니다), 그리고 여러 요청에 걸쳐 흐름이 이어지는 경우에는 스티키 세션을 사용하세요 — 로그인, 장바구니, 페이지 매김된 결과 세트 — 그래서 세션이 중간에 깨지지 않습니다. 몇 분 동안 동일한 IP가 필요하고 그 후에 새로운 것이 필요할 때, 스티키 세션 대 회전 세션에서 트레이드오프가 설명되어 있습니다.

더 많은 IP보다 저렴한 방법: 요청을 줄이세요
- 가져오기 전에 중복 제거하세요. 대부분의 크롤링 큐는 추적 매개변수와 후행 슬래시가 있는 동일한 URL을 포함합니다 - 먼저 정규화하세요.
- 조건부 GET을 사용하세요. If-Modified-Since 또는 If-None-Match를 보내고 304는 대역폭의 일부만 차지하며 여전히 하나의 요청으로 계산됩니다.
- 렌더링된 HTML보다 JSON 엔드포인트를 선호하세요. 하나의 API 호출이 종종 10개의 페이지 가져오기를 대체하며, API 제한은 보통 문서화되어 있습니다.
- 개발 중에는 적극적으로 캐시하세요. 저장된 응답에 대해 파서를 다시 실행하면 요청도 차단도 발생하지 않습니다.
- 대상의 현지 시간대에서 비피크 시간에 스크래핑하세요. 같은 속도가 서버에 03:00에 덜 영향을 미치고 덜 조절됩니다.
- 변경된 것만 가져오세요. 주간 업데이트되는 카탈로그의 일일 전체 크롤링은 여섯 번의 낭비된 크롤링입니다.
대역폭 절약은 두 배의 이익을 줍니다 — 요청이 적으면 429도 적고 비용도 줄어듭니다, 이는 우리가 프록시 대역폭 비용 절감에서 주장하는 것과 같은 논리입니다. 그리고 전혀 속도 조절 인프라를 구축하고 싶지 않다면, QuantumProxies Scraper API는 재시도, 회전 및 도메인별 조절을 하나의 엔드포인트 뒤에서 처리하여 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는 오류 클래스가 아닌 구성 파일의 숫자가 됩니다.