Повторы и таймауты HTTP: чек-лист перед продакшеном
Повторить HTTP-запрос после сбоя легко, но без ограничений это превращается в шторм запросов и дублирование операций. Разбираем таймауты, идемпотентность, backoff, Retry-After и метрики на примере Python 3.12.

Сервис, который повторяет каждый неудачный HTTP-запрос три раза, не становится надёжнее автоматически: при пяти секундах на попытку один вызов может занять 15 секунд, а запись заказа — выполниться дважды. Ниже — практический чек-лист для исходящих HTTP-вызовов на Python 3.12 с Requests 2.32.x и urllib3 2.x: какие ошибки повторять, где ставить таймаут, как добавить backoff и что проверить до выката.
Сначала определите границы операции
Повтор — это не свойство запроса, а решение о том, можно ли безопасно выполнить операцию ещё раз. Для чтения профиля или проверки курса валют повтор обычно допустим. Для создания платежа, заказа или тикета — только если внешний API поддерживает идемпотентный ключ и гарантирует одинаковый результат при повторной отправке.
Идемпотентность — свойство операции, при котором повтор того же запроса не меняет итог после первого успешного выполнения. HTTP-методы GET, HEAD и OPTIONS обычно проектируют как идемпотентные, но конкретный API может нарушить это ожидание. POST нельзя считать безопасным для повтора только по названию метода.
- Запишите для каждого endpoint, допустим ли повтор.
- Уточните у владельца API, поддерживает ли он Idempotency-Key или другой ключ операции.
- Разделите ошибки соединения, таймауты и ответы сервера с кодами 5xx.
- Определите максимальное время всей операции, а не только одной попытки.
- Заранее решите, что увидит пользователь после исчерпания попыток: ошибку, отложенную обработку или статус «проверяется».
Повтор должен спасать от временного сбоя, а не маскировать неизвестный результат операции.
Три подхода и их цена
У команды обычно есть три рабочих варианта. Выбор зависит от того, что важнее для конкретного вызова: быстрая реакция, устойчивость к краткому сбою или контроль над очередью повторных попыток.
| Подход | Когда подходит | Цена и риск |
|---|---|---|
| Без автоматических повторов | Платежи, создание сущностей без ключа идемпотентности, быстрые пользовательские запросы | Временный сбой сразу виден пользователю; нужен повтор на уровне очереди или интерфейса |
| Ограниченные повторы в HTTP-клиенте | GET, HEAD и безопасные операции с коротким временем ответа | Простой запуск, но легко увеличить задержку и нагрузку на зависимость |
| Повтор через очередь | Долгие интеграции, вебхуки, синхронизация и фоновые задачи | Нужно хранить состояние, ограничивать очередь и обрабатывать дубликаты |
Для синхронного запроса из веб-приложения обычно достаточно одного-двух ограниченных повторов для чтения. Для создания заказа лучше вернуть управление вызывающему коду, сохранить задачу и повторять её в фоне. Это увеличивает сложность, зато пользовательский запрос не держит соединение десятки секунд.
Настройте таймауты по слоям
У HTTP-вызова есть несколько стадий: соединение с сервером, получение ответа и чтение тела. Таймаут подключения не должен подменять общий дедлайн операции. В Requests параметр timeout ограничивает ожидание сетевой активности, но не задаёт единый бюджет на все повторы.
Разделите connect и read
Короткий таймаут подключения защищает от недоступного адреса. Таймаут чтения выбирайте по контракту API и размеру ответа: для JSON на несколько килобайт он обычно меньше, чем для выгрузки файла. Значения нельзя безопасно назначить «на глаз» для всех endpoint: возьмите p95 и p99 задержки из метрик, добавьте запас и проверьте результат нагрузочным тестом.
import requests
response = requests.get(
"https://api.example.com/v1/profile",
timeout=(2.0, 5.0), # подключение и ожидание данных
)
response.raise_for_status()
profile = response.json()
Проверка: медленный или недоступный сервер должен завершить вызов исключением в пределах заданного таймаута, а не оставить рабочий поток ждать бесконечно.
Считайте бюджет всей операции
Если на одну попытку разрешены пять секунд, а разрешены три попытки, фактическое ожидание может приблизиться к 15 секундам плюс задержки backoff. Для веб-запроса это часто больше таймаута балансировщика или браузера. Перед продакшеном зафиксируйте формулу: время попыток, паузы между ними, сериализация ответа и запас на код приложения.
Наружный дедлайн должен быть короче таймаута вызывающего слоя. Иначе внутренний клиент продолжит работу после того, как пользователь уже получил 504, и создаст лишнюю нагрузку.
Добавьте повторы только для временных ошибок
Повторять любой статус, отличный от 200, опасно. Ответ 400 обычно означает ошибку запроса, 401 — проблему авторизации, а 404 может быть нормальным результатом поиска. Такие ответы повтором не исправить.
Безопасная базовая политика для чтения: повторять сбои подключения, отдельные сетевые ошибки, таймаут чтения и ограниченный набор 5xx. Список статусов согласуйте с документацией конкретного API. Код 429 требует уважать ограничение частоты и заголовок Retry-After, если сервер его прислал.
from requests import Session
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
retry_policy = Retry(
total=2,
connect=2,
read=2,
status=2,
backoff_factor=0.5,
status_forcelist=(429, 500, 502, 503, 504),
allowed_methods=frozenset({"GET", "HEAD", "OPTIONS"}),
respect_retry_after_header=True,
raise_on_status=False,
)
session = Session()
session.mount(
"https://",
HTTPAdapter(max_retries=retry_policy),
)
response = session.get(
"https://api.example.com/v1/profile",
timeout=(2.0, 5.0),
)
response.raise_for_status()
Фрагмент предназначен для Requests 2.32.x и urllib3 2.x. Он не делает POST безопасным: метод не входит в allowed_methods. Если API поддерживает ключ идемпотентности, добавьте его по правилам этого API и отдельно протестируйте поведение при обрыве ответа.
Backoff — увеличивающаяся пауза между попытками. Он уменьшает синхронный наплыв клиентов после восстановления сервиса. Фиксированная задержка без случайной добавки опасна: тысячи экземпляров могут повторить запрос в одну и ту же секунду.
Когда выбирать каждый подход
Синхронный вызов
Выбирайте его, если результат нужен прямо сейчас, а весь бюджет укладывается в таймаут пользовательского запроса. Ограничьте число попыток и логируйте итоговую длительность. Для чтения можно начать с одной повторной попытки, но подтвердите решение измерениями, а не привычкой.
Фоновая очередь
Она подходит для интеграций, где ответ можно получить позже. Задаче нужны идентификатор, число попыток, время следующей попытки и причина последней ошибки. После исчерпания лимита отправляйте её в отдельное хранилище неуспешных задач, а не запускайте бесконечный цикл.
Без повтора
Отключайте автоматические повторы, когда неизвестно, успела ли зависимость выполнить побочный эффект. Для платежа безопаснее показать состояние «обработка» и запросить результат отдельной проверкой, чем отправить новый платёжный запрос вслепую.
Что ломается первым
- Бесконечные повторы. Процесс не падает, но забивает пул соединений и усиливает аварию зависимости. Нужны общий лимит, дедлайн и предельное число попыток.
- Повтор POST без ключа операции. Первый запрос мог дойти до сервера, а ответ потерялся по пути. Повтор создаёт вторую сущность или второе списание.
- Один таймаут на всё. Быстрый connect timeout не защищает от зависшего чтения тела. Передавайте таймаут явно для каждого типа вызова.
- Логирование только финальной ошибки. Без номера попытки и причины нельзя понять, был ли это 503, timeout или исчерпанный дедлайн.
- Одинаковый backoff у всех экземпляров. Сервисы повторяют запросы одновременно. Используйте случайную добавку, если это поддерживает выбранная библиотека или слой очереди.
Чек-лист перед продакшеном
- Для каждого endpoint указаны метод, допустимость повтора, ожидаемые статусы и максимальный размер ответа.
- Таймаут подключения и таймаут чтения заданы явно. Бесконечного ожидания нет.
- Есть бюджет всей операции, который меньше таймаута внешнего прокси или балансировщика.
- Повторы ограничены по числу и времени. Для 429 проверяется
Retry-After. - POST с побочным эффектом защищён идемпотентным ключом либо не повторяется автоматически.
- В логах есть endpoint без секретов, статус, номер попытки, длительность, причина сбоя и correlation ID.
- Метрики разделяют успешные вызовы с первой попытки, успешные после повтора, исчерпанные попытки и таймауты.
- В тестах воспроизведены медленное подключение, обрыв после отправки тела, 429, 503, некорректный JSON и большой ответ.
- Есть лимит на параллельные вызовы к одной зависимости. Повтор не должен обходить этот лимит.
Что делать дальше
Соберите таблицу всех исходящих HTTP-вызовов и добавьте в неё фактические p50, p95 и p99 задержки, долю ошибок и число повторов. Затем задайте отдельные политики для чтения, записи и фоновых задач. После выката смотрите не только на процент успешных ответов, но и на долю запросов, которым понадобилась вторая попытка: именно она первой показывает деградацию внешней зависимости.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: Requests: Advanced Usage, urllib3: Retry reference
Частые вопросы
Нужно ли повторять любой запрос после таймаута?
Сколько повторов поставить по умолчанию?
Чем connect timeout отличается от read timeout?
Что делать, если я не успеваю в общий таймаут веб-запроса?
Можно ли повторять POST через Retry из urllib3?
Что проверить, если пример не работает на другой версии Python или Requests?
Какие подводные камни видны только в продакшене?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.