Повтор запроса при ошибке в Python: с чего начать
Как за 15 минут добавить автоматический повтор к HTTP-запросам на requests и httpx, чтобы разовый сбой сети не ронял скрипт. Что установить, что скопировать и как проверить, что повтор реально работает.

Скрипт качает данные с чужого API, и раз в сотню запросов прилетает ConnectionError или 503 — сервер моргнул на долю секунды. Без повтора весь запуск падает. С повтором тот же самый запрос уходит второй раз через секунду и проходит. В этом гайде вы добавите автоматический повтор к обычным HTTP-запросам: сначала готовым инструментом за пять строк, потом руками, чтобы понимать, что происходит. Займёт минут пятнадцать.
Что нужно иметь до начала
Гайд рассчитан на Python 3.9+ и две популярные библиотеки. Ставим то, чем пользуетесь:
- Python 3.9 или новее — проверьте командой
python --version. - Пакетный менеджер pip — идёт вместе с Python.
- Одну из библиотек для запросов:
requests(синхронная, самая распространённая) илиhttpx(умеет и синхронно, и асинхронно). - Отдельную библиотеку для повторов —
tenacity. Она универсальная и не привязана к конкретному HTTP-клиенту.
Установка:
pip install requests tenacity
Проверить, что всё встало, можно так:
python -c "import requests, tenacity; print(requests.__version__, tenacity.__version__)"
Должны напечататься две версии через пробел, например 2.32.3 9.0.0. Если вместо этого ModuleNotFoundError — пакет не установился, вернитесь на шаг с pip.
Что вообще стоит повторять
Повтор помогает только с временными сбоями. Их называют transient — сбоями, которые проходят сами.
Повторять запрос, который вернул 404, бессмысленно: ресурса нет и через секунду не появится. А вот 503 «сервис временно недоступен» — классический кандидат на повтор.
Ориентир, что повторять, а что нет:
- Повторять: ошибки соединения и таймауты (
ConnectionError,Timeout), коды429,500,502,503,504. - Не повторять:
400,401,403,404,422— это ошибки в самом запросе, повтор ничего не изменит.
Отдельная оговорка: повторять безопасно только идемпотентные операции — те, что при повторе не создают дубль. GET повторять можно всегда. POST, который создаёт заказ или списывает деньги, — только если сервер поддерживает ключ идемпотентности, иначе рискуете оформить два заказа вместо одного.
Шаг 1. Самый простой повтор через tenacity
Оборачиваем функцию с запросом декоратором. Скопируйте целиком и запустите:
import requests
from tenacity import retry, stop_after_attempt, wait_fixed
@retry(stop=stop_after_attempt(3), wait=wait_fixed(1))
def fetch_data(url):
# Пробуем не больше 3 раз, между попытками ждём 1 секунду
response = requests.get(url, timeout=5)
response.raise_for_status() # превращает 4xx/5xx в исключение
return response.json()
if __name__ == "__main__":
data = fetch_data("https://httpbin.org/status/503")
print(data)
Адрес httpbin.org/status/503 всегда отвечает кодом 503 — удобно для проверки. Запустите: скрипт трижды дёрнет URL и после третьей неудачи упадёт с RetryError. Это правильное поведение — сервер стабильно отдаёт ошибку, повторы её не лечат.
Чтобы увидеть успешный сценарий, поменяйте URL на https://httpbin.org/json. Функция вернёт словарь с первого раза.
Шаг 2. Повторять только нужные ошибки
Код выше повторяет вообще любое исключение — включая 404, что бесполезно. Ограничим повтор конкретными ситуациями через retry_if_exception_type:
import requests
from requests.exceptions import ConnectionError, Timeout
from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_exception_type
@retry(
stop=stop_after_attempt(3),
wait=wait_fixed(1),
retry=retry_if_exception_type((ConnectionError, Timeout)),
)
def fetch_data(url):
response = requests.get(url, timeout=5)
response.raise_for_status()
return response.json()
Теперь повтор срабатывает только на проблемах с сетью и таймаутах. Ошибка 404 вылетит сразу, без трёх бесполезных попыток.
Проблема: raise_for_status() на код 503 бросает HTTPError, а не ConnectionError. Чтобы повторять и по коду ответа, проверяйте статус вручную и бросайте своё исключение:
import requests
from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_exception_type
RETRYABLE_STATUS = {429, 500, 502, 503, 504}
class RetryableError(Exception):
"""Ошибка, которую имеет смысл повторить."""
@retry(
stop=stop_after_attempt(3),
wait=wait_fixed(1),
retry=retry_if_exception_type(RetryableError),
)
def fetch_data(url):
response = requests.get(url, timeout=5)
if response.status_code in RETRYABLE_STATUS:
# Временный сбой сервера — просим tenacity повторить
raise RetryableError(f"Статус {response.status_code}")
response.raise_for_status() # остальные 4xx/5xx — сразу наружу
return response.json()
Проверьте на https://httpbin.org/status/503 — увидите три попытки. На https://httpbin.org/status/404 — падение с первого раза.
Шаг 3. Экспоненциальная пауза вместо фиксированной
Фиксированная пауза в секунду плоха под нагрузкой: если сервер лёг, десятки клиентов бьют по нему в одном ритме и добивают. Правильнее увеличивать паузу с каждой попыткой — 1, 2, 4 секунды. Это называется экспоненциальная задержка (exponential backoff).
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(4),
wait=wait_exponential(multiplier=1, min=1, max=10),
)
def fetch_data(url):
...
wait_exponential(multiplier=1, min=1, max=10) даёт паузы 1, 2, 4, 8 секунд, но не больше 10. Чтобы клиенты не синхронизировались, добавьте случайный разброс — jitter — через wait_random_exponential вместо wait_exponential; сигнатура та же.
Если у вас httpx вместо requests
Код почти не меняется — tenacity не привязана к клиенту. Отличаются только имена исключений: у httpx это httpx.HTTPError и его подклассы httpx.ConnectError, httpx.TimeoutException. Логика та же: проверяйте response.status_code и бросайте своё RetryableError на временные коды.
Точные имена классов исключений для вашей версии смотрите в документации httpx — они менялись между мажорными релизами.
Готовые паузы: сравнение вариантов
| Функция паузы | Как ведёт себя | Когда брать |
|---|---|---|
wait_fixed(1) | Всегда одна секунда | Скрипт для себя, один клиент |
wait_exponential(...) | 1, 2, 4, 8 с | Запросы к нагруженному API |
wait_random_exponential(...) | Как выше, но с разбросом | Много клиентов сразу |
Если не получилось
ModuleNotFoundError: No module named 'tenacity'— пакет не установлен в том окружении, где запускаете скрипт. Проверьте, чтоpip installиpythonиз одного venv.- Повтор не срабатывает, ошибка вылетает сразу — тип исключения не совпадает с указанным в
retry_if_exception_type. Уберите фильтр временно, посмотрите реальный класс исключения в трейсбеке, добавьте именно его. - Скрипт висит очень долго — забыли
timeoutв самом запросе. Без негоrequests.getможет ждать бесконечно, и tenacity не поможет: повтор запустится, только когда запрос сам завершится с ошибкой. RetryErrorпосле всех попыток — это нормально, если сервер стабильно отдаёт ошибку. Оберните вызов вtry/except tenacity.RetryErrorи решите, что делать: залогировать и пропустить или прервать работу.- POST-запрос создал дубль — вы повторили неидемпотентную операцию. Повторяйте POST только с ключом идемпотентности на стороне API либо не повторяйте вовсе.
Что настроить дальше
Когда базовый повтор работает, посмотрите в сторону:
- Логирования попыток — у
tenacityесть параметрbefore_sleep, куда можно передать функцию, печатающую номер попытки. Полезно, чтобы видеть, что повторы реально происходят. - Заголовка
Retry-After— при коде 429 сервер часто сам говорит, через сколько секунд повторить. Вычитывать паузу из ответа сложнее, чем фиксированный backoff, но вежливее к чужому API. - Общего бюджета времени —
stop_after_delay(30)вместо счётчика попыток ограничивает не количество, а суммарное время повторов.
Инвалидация кеша и работа с очередями задач при массовых повторах — отдельные темы, здесь мы разобрали только точечный повтор одиночного запроса.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: tenacity — официальная документация, Requests: HTTP for Humans — документация
Частые вопросы
У меня старая версия tenacity, синтаксис другой?
Можно обойтись без tenacity и написать повтор вручную?
Почему повтор не сработал на коде 503?
Сколько попыток ставить?
Повтор POST-запроса безопасен?
Работает ли это с асинхронным кодом на asyncio?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.