Ошибка 429 при парсинге: как считать паузы и ретраи, а не гадать
429 Too Many Requests означает, что вы превысили частоту, допустимую для одного адреса. Ключевое слово — «для одного»: это не лимит вашего аккаунта и не признак блокировки, а сигнал о темпе, который считается арифметикой.
Retry-After — единственный честный сигнал
Вместе с 429 сервер может прислать заголовок Retry-After: в секундах или датой. Если он есть, гадать не нужно — там написано, сколько ждать. Игнорировать его бессмысленно: повторный запрос раньше срока почти всегда даёт тот же 429 и продлевает наказание.
Хорошая новость: в стандартном механизме повторов для requests уважение к Retry-After включено по умолчанию и действует для кодов 413, 429 и 503. Отдельно настраивать нечего.
Что повторять, а что бессмысленно
Повторять имеет смысл только то, что может измениться само: 408, 429, 500, 502, 503, 504, а у сайтов за Cloudflare ещё 522 и 524. Остальные четырёхсотые повторять бесполезно — тот же запрос даст тот же ответ.
- 400, 404, 405, 410, 422 — ошибка в самом запросе, ретрай её не исправит.
- 401, 403, 407 — вопрос учётных данных или адреса, а не темпа.
- 429 — повторять, но с растущей паузой.
Экспоненциальная задержка и зачем нужен джиттер
Стандартная схема: пауза удваивается с каждой попыткой. Важная деталь реализации, которая многих удивляет: первый повтор идёт без паузы вообще, а множитель начинает действовать со второго. При коэффициенте 0,5 последовательность выходит такой: 0 с, 0,5 с, 1 с, 2 с, 4 с.
Джиттер — случайная добавка к паузе. Без него все потоки, получившие 429 одновременно, отсчитают одинаковую задержку и ударят снова тоже одновременно, то есть залпом. Джиттер разводит их по времени и превращает залп в поток.
import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
retry = Retry(
total=5,
backoff_factor=0.5, # 0 с, 0.5, 1, 2, 4 ... первый ретрай без паузы
backoff_jitter=0.3, # требует urllib3 >= 2.0
backoff_max=30, # потолок паузы, по умолчанию 120 с
status_forcelist=(429, 500, 502, 503, 504),
respect_retry_after_header=True, # включено по умолчанию
raise_on_status=False, # вернуть ответ, а не бросить исключение
)
session = requests.Session()
# pool_maxsize не меньше числа потоков, иначе соединения будут пересобираться
session.mount("https://", HTTPAdapter(max_retries=retry, pool_maxsize=32, pool_block=True))
r = session.get("https://example.com", timeout=(5, 30),
proxies={"https": "http://ЛОГИН:ПАРОЛЬ@ШЛЮЗ:ПОРТ"})В Scrapy backoff устроен иначе
Здесь легко ошибиться, ожидая привычного поведения. Встроенный механизм повторов в Scrapy включён по умолчанию, но экспоненциальной задержки в нём нет: повторный запрос просто возвращается в планировщик и уходит в общей очереди. Темп задают другие настройки.
# settings.py
# RetryMiddleware включён по умолчанию, но экспоненциальной задержки в нём нет:
# повторный запрос просто возвращается в планировщик. Темп задают эти настройки.
RETRY_TIMES = 4
RETRY_HTTP_CODES = [500, 502, 503, 504, 522, 524, 408, 429]
DOWNLOAD_DELAY = 1
RANDOMIZE_DOWNLOAD_DELAY = True # множитель 0.5-1.5 к задержке
AUTOTHROTTLE_ENABLED = True # адаптивная пауза по времени ответа сервера
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0Практический вывод: в Scrapy не ищите настройку экспоненциальной паузы — вместо неё включайте адаптивное регулирование, которое само подстраивает задержку под время ответа сервера. Это работает лучше фиксированных значений, потому что нагрузка на целевой сайт меняется в течение суток.
Что менять первым
Порядок действий при устойчивых 429 такой: сначала пауза, потом число потоков, и только потом размер пула адресов. Причина в том, что 429 считается на адрес: добавив потоков без снижения частоты, вы получите те же 429, но быстрее.
И обратное соображение, важное для расчёта бюджета: на ротационных тарифах трафик не лимитируется, а тарификация идёт по числу одновременных подключений. Значит повторный запрос ничего не стоит дополнительно — переспросить дешевле, чем потерять данные и собирать их заново.
Что повторять нельзя даже с задержкой
Список кодов из предыдущего раздела стоит перевернуть: важнее знать, что повторять бессмысленно, потому что именно на этом теряют время. Правило простое — повторяют то, что может измениться само.
- Ошибка в самом запросе — повтор вернёт то же самое, сколько бы вы ни ждали.
- Вопрос учётных данных или адреса — лечится сменой пары логин-пароль или whitelist, не паузой.
- Отсутствующая страница — её не появится.
Отдельно про метод запроса. По умолчанию механизм повторов в requests не повторяет отправку данных: считается, что повторная отправка может создать дубль на стороне сервера. Если вам нужно повторять и её, разрешать это следует осознанно и только там, где повторная отправка безопасна.
Где смотреть, что происходит
Устойчивые отказы удобно диагностировать по распределению кодов, а не по отдельным ошибкам. Полезно логировать не только факт отказа, но и то, с какой попытки запрос прошёл.
- Доля отказов растёт с числом потоков — упёрлись в частоту, снижайте темп.
- Отказы приходят пачками через равные интервалы — сработала синхронизация потоков, не хватает джиттера.
- Отказы не зависят от темпа вообще — дело не в частоте, проверяйте авторизацию и заголовки.
Пул, в котором ретраи бесплатны
Тарификация по числу одновременных подключений, трафик не лимитируется. IPv6 от 650 ₽ за 50 потоков, IPv4 от 1375 ₽ за 100.
Смотреть тарифыПрокси под эту задачу
Проверить нашими инструментами
Читайте также
- Один шлюз вместо списка прокси: как настроить софт, который ждёт списокПарсеры считают масштаб числом прокси, а ротационный шлюз — это одна строка. Разбор по программам: где переключить режим, где продублировать строку и где честный ответ — купить список.
- 403 через прокси: отказал сайт, отказал прокси или дело вообще не в адресе403 приходит и от прокси, и от целевого сайта, и различить их можно не всегда. Разбор: где смотреть код Cloudflare, какие коды не лечатся сменой адреса и почему заголовки прокси — плохой тест.
- IP не меняется при ротации: виноват keep-alive, а не проксиАдрес выхода выбирается при установке соединения, а не при отправке запроса. Любой клиент с пулом соединений держит один IP — разбор механизма и однострочные починки для requests, httpx и aiohttp.
