Многие разработчики ставят timeout=5 в вызове requests и считают, что запрос гарантированно завершится за пять секунд. На деле это не так. Параметр timeout в библиотеке requests — это не общий дедлайн на всю операцию, а два независимых ограничения: время на установку соединения (connect) и время ожидания между отправкой данных и получением ответа (read). По умолчанию таймаута нет вообще, и клиент может висеть часами, если сервер не отвечает или сеть «залипла». В этой статье разберём, как работают таймауты, как настроить ретраи без шторма запросов и как ограничить всю операцию бюджетом времени.

Connect и read: два разных таймаута

Когда вы передаёте в requests одно число, например timeout=5, оно применяется и к connect, и к read. Connect-таймаут — это максимальное время на установку TCP-соединения (включая DNS-резолвинг и TLS-хендшейк). Read-таймаут — это максимальное время ожидания между получением порций данных от сервера. Важно: read-таймаут не ограничивает общее время чтения ответа. Если сервер медленно отправляет данные, каждый байт приходит в пределах таймаута, и запрос может длиться сколько угодно.

Пример: сервер отправляет ответ по частям с задержкой 4 секунды между пакетами. При timeout=5 запрос будет выполняться 10, 20, 30 секунд и дольше — столько, сколько потребуется серверу. Это часто становится неожиданностью. Чтобы контролировать общее время, нужен отдельный механизм — бюджет времени на всю операцию.

Ретраи: как не устроить шторм и не задублировать POST

Автоматические ретраи при ошибках сети или ответах 5xx — полезная практика, но при неаккуратной настройке они превращаются в шторм запросов, особенно если несколько клиентов одновременно повторяют попытки. В requests ретраи настраиваются через urllib3.Retry, который передаётся в HTTPAdapter. Основные параметры:

  • total — общее количество повторных попыток (включая первую? Нет, total — это число повторов после первой неудачи).
  • connect, read, status — отдельные лимиты для ошибок соединения, чтения и HTTP-статусов.
  • backoff_factor — множитель для экспоненциальной задержки между попытками (например, 0.5 даёт задержки 0, 1, 2, 4 секунды...).
  • status_forcelist — список кодов ответа, при которых нужно повторить запрос (например, [500, 502, 503, 504]).
  • allowed_methods — методы, для которых разрешены повторы (по умолчанию только идемпотентные: GET, HEAD, PUT, DELETE, OPTIONS, TRACE).

Типичная ошибка — разрешить ретраи для POST. Если сервер получил запрос, но ответ потерялся, повторная отправка может создать дубль (например, дважды списать деньги). Если идемпотентность не гарантирована, лучше не ретраить POST автоматически или использовать идемпотентные ключи.

Ещё одна ошибка — устанавливать total слишком большим (например, 10) без ограничения общего времени. При backoff_factor=1 задержки растут экспоненциально, и суммарное время может достигать минут. Рекомендуется комбинировать ретраи с общим бюджетом времени.

Бюджет времени на всю операцию

Чтобы гарантировать, что запрос (включая все ретраи) не превысит заданное время, нужен внешний таймаут. В requests нет встроенного параметра для общего дедлайна. Простые способы:

  • Использовать signal.alarm (только в основном потоке Unix) для прерывания по сигналу.
  • Обернуть вызов в отдельный поток и использовать ThreadPoolExecutor с таймаутом.
  • Применить асинхронный клиент (например, httpx с таймаутами) или библиотеку с поддержкой deadline.

Пример с ThreadPoolExecutor:

from concurrent.futures import ThreadPoolExecutor, TimeoutError import requests def fetch(url): return requests.get(url, timeout=(3, 5)) with ThreadPoolExecutor() as executor: future = executor.submit(fetch, 'https://example.com') try: response = future.result(timeout=10) except TimeoutError: print('Превышен общий бюджет времени')

Здесь connect-таймаут 3 секунды, read-таймаут 5 секунд, а общий дедлайн — 10 секунд. Если запрос не уложился, future.result возбудит TimeoutError. Однако учтите, что поток продолжит выполняться в фоне, если не добавить отмену.

Практические рекомендации и типичные ошибки

Вот несколько советов, которые помогут избежать проблем:

  • Всегда указывайте таймауты явно. Даже если кажется, что сервер быстрый, сеть может подвести.
  • Разделяйте connect и read таймауты: connect обычно меньше (2-5 секунд), read — больше (10-30 секунд), в зависимости от ожидаемого времени ответа.
  • Не устанавливайте timeout=None и не полагайтесь на значения по умолчанию.
  • Для ретраев используйте разумные значения: total=3, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504].
  • Не ретрайте POST без идемпотентности.
  • Комбинируйте ретраи с общим бюджетом времени, чтобы избежать длительных зависаний.
  • Логируйте таймауты и ретраи — это поможет диагностировать проблемы.

Типичный пример ошибки: разработчик ставит timeout=5 и urllib3.Retry(total=5, backoff_factor=1). В случае сбоя сервера запрос будет повторяться с задержками 0, 1, 2, 4, 8 секунд, и общее время превысит 15 секунд, хотя разработчик ожидал максимум 5. Или другой случай: read-таймаут 5 секунд, но сервер отправляет данные медленно, и запрос длится минуту — таймаут не срабатывает, потому что данные приходят регулярно.

Помните: timeout в requests — это инструмент контроля на уровне сокета, а не на уровне операции. Для полного контроля используйте комбинацию таймаутов, ретраев и внешнего дедлайна. Тестируйте поведение на локальном стенде с имитацией задержек, чтобы убедиться, что настройки работают как задумано.