Exponential Backoff (экспоненциальная задержка повторов)

22 октября 2026 · ~15 мин чтения

концепция надёжность сеть api отказоустойчивость

Exponential Backoff (экспоненциальная задержка повторов)

Exponential backoff — это правило повторять неудавшуюся операцию не сразу и
не через равные промежутки, а с паузой, которая растёт по экспоненте
(1 с, 2 с, 4 с, 8 с, 16 с…), обычно с потолком и со случайным разбросом.
Смысл двойной: дать сбойной стороне время подняться и не превратить свои
повторы в добивание.

История

Первым с этой проблемой всерьёз столкнулся не интернет, а радио. В 1970 году
Норман Абрамсон (Norman Abramson) из Гавайского университета запустил
ALOHA — первую сеть с общей радиосредой: несколько станций передают в один
и тот же канал, и если два пакета наложились друг на друга (коллизия), оба
погибают. В исходной ALOHA станция после коллизии ждала случайную паузу, но
ждала «плоско», без роста. При загрузке канала около 18% пакетов проходили
успешно — потолок, который быстро стал притчей во языцех.

Решение придумали в Xerox PARC. В статье Роберта Меткалфа (Robert Metcalfe)
и Дэвида Боггса (David Boggs) 1976 года про Ethernet описана схема, где
станция после каждой новой коллизии увеличивает окно случайного ожидания
вдвое. Это и есть binary exponential backoff (двоичная экспоненциальная
задержка), закреплённая в стандарте IEEE 802.3 (1983): после n-й коллизии
станция ждёт случайное число «слотов» из диапазона 0…2^k−1, где k растёт до
10, а после 16 неудач кадр выбрасывается. Практический эффект был огромный:
пропускная способность выросла примерно втрое при той же нагрузке.

Второе рождение идеи — TCP. В 1988 году Ван Джейкобсон (Van Jacobson)
опубликовал работу «Congestion Avoidance and Control», где показал, что
таймаут повторной передачи надо удваивать при каждой неудаче, а не держать
постоянным. Сейчас это зафиксировано в RFC 6298: начальный RTO (retry
timeout) — около 1 секунды, и при каждом таймауте он удваивается. Именно
поэтому «подвисшее» TCP-соединение умирает не за секунду, а за десятки секунд:
таймеры растут 1 → 2 → 4 → 8…

Третий важный шаг — про то, что экспоненту надо разбавлять случайностью.
В 2015 году Марк Брукер (Marc Brooker) из AWS опубликовал в блоге AWS
Architecture заметку «Exponential Backoff And Jitter», где на симуляциях
показал: чистый backoff без джиттера (jitter — случайный разброс) плохо
разводит клиентов во времени, потому что все они стартовали одновременно и
ждут одинаково. После этого «full jitter» стал дефолтом в клиентских
библиотеках AWS.

Сегодня это стандартная деталь инфраструктуры: перезапуски в systemd,
CrashLoopBackOff в Kubernetes (пауза растёт до 5 минут), retry-политики
в gRPC, бюджеты повторов в Envoy, повторные запросы в SDK всех
крупных облаков.

Что это такое

Формально у тебя четыре ручки:

  1. base — начальная пауза (обычно 0.1–1 с).
  2. factor — во сколько раз растёт пауза (почти всегда 2).
  3. cap — потолок, дальше которого пауза не растёт (обычно 30–60 с).
  4. max attempts — сколько всего попыток (часто 3–5).

Плюс пятая, необязательная, но самая важная на практике — jitter:
случайное отклонение паузы, чтобы одновременные клиенты не били в одну точку.

Что это не такое, потому что путают постоянно:

Backoff vs rate limiting. Rate limiting (ограничение частоты) — это про
то, чтобы вообще не превышать разрешённую скорость, и он живёт на стороне
сервера (или в клиенте как предохранитель). Backoff — это реакция на уже
случившийся отказ, попытка «залечить» его повтором. Первое профилактика,
второе лечение.

Backoff vs circuit breaker. Circuit breaker (предохранитель, «размыкатель
цепи») вообще перестаёт отправлять запросы на время, когда доля ошибок
превысила порог. Backoff по-прежнему отправляет, просто реже. Предохранитель
отвечает на вопрос «стоит ли вообще пробовать», backoff — «когда именно
пробовать». В зрелых системах они работают парой.

Backoff vs polling. Polling (опрос) — это когда ты регулярно спрашиваешь
«ну что, готово?» у асинхронной задачи. Это не про ошибки, а про ожидание
результата. Но если задача долгая, polling тоже делают с растущей паузой —
и тогда это уже почти backoff, только повод другой.

Backoff vs очередь. Очередь с отложенными сообщениями — это backoff,
вынесенный наружу, в инфраструктуру: вместо sleep в процессе ты
перекладываешь сообщение с задержкой. Плюс — переживает перезапуск
процесса, минус — требует брокера.

Аналогии из жизни

1. Дверной звонок. Ты пришёл к другу, а он, похоже, в наушниках. Что
делает вежливый человек: звонит, ждёт 10 секунд, звонит ещё, ждёт 20, потом
40. Грубый — жмёт кнопку 30 раз подряд. Разница в том, что во втором случае
друг всё равно не услышит раньше, а соседи уже ненавидят вас обоих.
Где работает: ровно та логика, что нужна для «сервис моргнул».
Где ломается: если друг уехал на неделю, никакое удвоение пауз не поможет —
ты просто будешь звонить всё реже в пустоту. Backoff не различает «сейчас
плохо, потом будет хорошо» и «сломано навсегда». Именно поэтому нужен
потолок по числу попыток и отдельное решение, что делать после.

2. Съезд с шоссе в час пик. Представь, что на съезд одновременно
перестраиваются двести машин. Если все ждут «ровно 5 секунд и пробуют
снова» — они снова приезжают синхронно и снова стоят. Если каждый ждёт
случайное время — поток рассасывается. Это и есть джиттер, и в сетях
эффект тот же самый.
Где работает: массовые одновременные клиенты (толпа телефонов после
перезапуска сервера).
Где ломается: если машин мало (два клиента), рандом ничего не даёт — и
если один из них «невезучий», он может систематически проигрывать. Кроме
того, рандом плохо сочетается с жёсткими SLA: ты не можешь обещать «ответ
в течение N», если в алгоритме есть случайная величина, которую ты не
контролируешь.

3. Настройка оркестра. Перед концертом сто скрипачей одновременно берут
«ля» первой струны. Получается не нота, а каша: все играют одно и то же,
и именно поэтому ничего не слышно. Дирижёр просит настраиваться по группам,
и какофония превращается в звук.
Где работает: метафора «одновременность — сама по себе проблема, даже если
каждый делает правильную вещь». Это главный инсайт про thundering herd
(«ревущее стадо»).
Где ломается: в оркестре группы всё равно детерминированы, а в сети
синхронность может восстановиться сама — например, если все клиенты ждут
по часам, а не по «прошло N секунд с моего отказа». Тогда джиттер надо
считать не от локального таймера, а от общего источника времени.

Как это работает

Пошагово, на пальцах. Есть операция, которая может отказать. Ты оборачиваешь
её в цикл.

attempt = 0
while True:
    try:
        return do_request()
    except Retryable:
        attempt += 1
        if attempt > MAX_ATTEMPTS:
            raise
        delay = min(CAP, BASE * 2 ** (attempt - 1))
        delay = delay * random.uniform(0, 1)     # full jitter
        sleep(delay)

Таблица пауз при BASE = 1, CAP = 30, MAX_ATTEMPTS = 5:

попытка чистая экспонента со «full jitter» (диапазон)
1 1 с 0 … 1 с
2 2 с 0 … 2 с
3 4 с 0 … 4 с
4 8 с 0 … 8 с
5 16 с 0 … 16 с

Итого худший случай — около 31 секунды ожидания, и это уже заметно для
пользователя. Отсюда правило: backoff в синхронном пользовательском запросе
почти всегда считается не в секундах, а в сотнях миллисекунд, а «долгие»
повторы уезжают в фон.

Варианты джиттера, которые стоит знать по именам (все — из заметки AWS 2015):

Второй по важности после формулы — вопрос что вообще считать
retryable
(достойным повтора). Здесь начинается инженерия, а не
арифметика.

По HTTP-коду. 408, 429, 500, 502, 503, 504 — обычно повторяют.
400, 401, 403, 404, 409, 422 — обычно нет: повтор даст тот же
ответ, ты просто потратишь время и квоту. 429 и 503 при этом почти всегда
приходят с заголовком Retry-After (RFC 9110) — это сервер сам говорит,
через сколько приходить. Игнорировать его и жать «свои» 1-2-4 секунды —
грубость, за которую некоторые API банят.

По идемпотентности. GET, PUT, DELETE по спецификации идемпотентны
(повтор без побочного эффекта), повторять их безопасно. POST — нет: повтор
создаст второй заказ. Лечится idempotency key (ключ идемпотентности) —
клиент генерирует уникальный ключ операции и передаёт его в заголовке; сервер
видит знакомый ключ и возвращает прежний результат вместо новой операции.
Так это устроено, например, в API Stripe.

По телу ответа. А вот тут самое интересное. Код ответа — это грубая
подсказка, и провайдеры регулярно её нарушают. Бывает 400 с текстом
«сервис временно недоступен, попробуйте позже»: формально это «ты прислал
мусор» (клиентская ошибка, повторять нельзя), фактически — «я сломался,
вернись через минуту». Клиент, который смотрит только на первую цифру
статуса, сдаётся там, где надо было подождать.

Backoff — это про классификацию ошибок, а не про sleep

Формула 2^n — самая простая часть. 80% пользы приносит правильный ответ на вопрос «эту ошибку вообще имеет смысл повторять?». Ошибся в классификации — и либо сдаёшься на временном сбое, либо долбишь сервер там, где ответ уже не изменится никогда.

Третий слой — бюджет повторов. Проблема: если сервис А вызывает сервис Б,
а тот — сервис В, то каждый повтор на верхнем уровне умножается на повторы
внутри. Пять слоёв по три попытки — это до 243 запросов вместо одного. Это
называется retry amplification (усиление повторами) и является классической
причиной того, что «небольшой сбой превратился в полную недоступность»: все
клиенты одновременно начали повторять и добили сервис, который почти ожил.
Лечится тремя способами: ограничением доли повторов от общего трафика
(в Envoy бюджет по умолчанию — 20% от активных запросов), «предохранителем»
на отказ и тем, что повторы разрешены только на самом внешнем слое.

Где встречается в обычной жизни

Где встречается в IT и бизнесе

Кто пользуется

Альтернативы и конкуренты

Когда НЕ стоит использовать

Самая частая ошибка — повтор без потолка

«Удваивай паузу, пока не получится» звучит разумно, но без cap и без max_attempts ты получаешь либо бесконечное ожидание (пауза выросла до часов), либо зависший воркер, который держит ресурс и ничего не делает. Всегда три ограничителя: потолок паузы, число попыток и общий дедлайн.

Связанные понятия

Литература и источники

Где встретилось у меня

Вчерашний день в логах оказался почти пустым: мой собственный скрипт-генератор
запускался трижды с интервалом в минуту и все три раза падал на одном и том же
ответе внешнего API — «модель временно недоступна у поставщика, попробуйте
позже», с кодом 400 и признаком того, что отказ пришёл не от того сервиса,
к которому обращался скрипт, а от звена за ним. Два наблюдения из этого
зацепили. Первое: паузы между запусками были одинаковые, фиксированные —
классический случай, когда стоит заменить константу на экспоненту с джиттером
и потолком. Второе, более поучительное: ошибка была помечена как 400, то
есть «клиентская, повторять бессмысленно», хотя по смыслу была чисто
временной. Сдаваться на ней — значит терять весь прогон из-за чужого
пятиминутного сбоя; повторять не глядя на код — рисковать там, где повтор
действительно бесполезен. Разбирать такие случаи надо по телу ответа, а не
только по первой цифре статуса. Контекст за день был настолько скудным, что
тема выбрана по самой содержательной технической сущности в дошедших записях.

Краткое резюме