Что означает ответ 429
RFC 6585 описывает статус 429 Too Many Requests как сигнал о превышении некоторого ограничения частоты. Ответ может содержать Retry-After, но его отсутствие не означает, что повтор разрешён в любой момент. Согласно HTTP-семантике, Retry-After может быть задержкой в секундах или HTTP-date. При отсутствии Retry-After scheduler должен применить явно заданную локальную политику, например ограниченную задержку или остановку с передачей решения оператору. Это не следует выдавать за указание сервера. Некорректное значение Retry-After нужно считать недействительным, а не пытаться угадать его смысл. Если серверное ожидание превышает max_wait, остановите задачу или передайте её оператору. Нельзя уменьшать Retry-After до локального cap и отправлять раньше указанного сервером срока. Если рассчитанное ожидание выходит за абсолютный deadline задачи, повтор не планируется. Дедлайн должен проверяться перед постановкой в очередь и непосредственно перед возможной отправкой. HTTP-date зависит от часов. Для измерения ожидания используйте монотонные часы, а wall-clock — только для разбора даты. При наличии рассинхронизации нельзя обещать точный момент восстановления. Если ответ содержит дату сервера, её можно использовать как дополнительный ориентир для оценки смещения, но результат всё равно должен быть ограничен локальным deadline.
Одна очередь вместо множества таймеров
Планирование и сетевую отправку лучше разделить. Планировщик принимает уже полученный результат 429 и возвращает одно из решений: wait с моментом следующей проверки, stop с причиной или escalate для ручного решения. Он не открывает соединения, не запускает HTTP-клиент и не утверждает, что будущий запрос будет успешным. Для каждой логической задачи нужен стабильный task_id или иной ключ дедупликации. В состоянии задачи храните число уже использованных попыток, абсолютный deadline, время готовности и признак active. При поступлении нового события для active-задачи нельзя создавать второй повторный таймер. При дублирующих событиях храните одну запись; новый более поздний срок нельзя заменять старым более ранним запуском. Пример сохраняет максимум из двух сроков. Пример ниже не включает сетевую отправку и рассчитан на одного локального владельца состояния. В многопоточном или распределённом worker проверка и изменение состояния требуют блокировки/транзакции. Функции получают уже проверенные числовые лимиты и часы; полноценная очередь и сохранение состояния не реализованы. Значения max_attempts, max_wait и fallback_delay должны задаваться политикой конкретного приложения, а не восприниматься как универсальные параметры.
import re
from email.utils import parsedate_to_datetime
class Decision:
def __init__(self, kind, ready_at=None, reason=None):
self.kind, self.ready_at, self.reason = kind, ready_at, reason
class RetryState:
def __init__(self, deadline_mono, attempts=1):
self.deadline_mono = deadline_mono
self.attempts = attempts # Includes the request that returned 429.
self.active = False
self.ready_at = None
def parse_retry_after(value, wall_now):
if value is None:
return "missing", None
text = value.strip()
if len(text) > 128:
return "invalid", None
if re.fullmatch(r"[0-9]+", text):
return "valid", int(text)
try:
date = parsedate_to_datetime(text)
if date.tzinfo is None:
return "invalid", None
return "valid", max(0, date.timestamp() - wall_now)
except (ValueError, TypeError, OverflowError):
return "invalid", None
def plan_after_429(state, retry_after, *, wall_now, mono_now,
safe_to_retry, uncertain=False, max_attempts=3,
max_wait=60, fallback_delay=2):
# Single-threaded example: a shared scheduler needs a lock/transaction.
def stop(reason):
state.ready_at = None
return Decision("stop", reason=reason)
if uncertain:
return stop("reconcile_unknown_outcome")
if not safe_to_retry:
return stop("retry_not_authorized_by_contract")
if state.active:
return Decision("stop", reason="already_active")
if state.attempts >= max_attempts:
return stop("attempt_budget_exhausted")
if mono_now >= state.deadline_mono:
return stop("deadline_exhausted")
kind, delay = parse_retry_after(retry_after, wall_now)
if kind == "invalid":
return stop("invalid_retry_after")
if kind == "missing":
delay = fallback_delay # An explicit local policy, not a server promise.
if delay < 0 or delay > max_wait:
return stop("wait_budget_exhausted") # Never clamp a server delay down.
ready = max(mono_now + delay, state.ready_at or mono_now)
if ready >= state.deadline_mono:
return stop("deadline_exhausted")
state.ready_at = ready # One slot per task, not a new timer each time.
return Decision("wait", ready_at=ready)
def claim_due(state, *, mono_now, max_attempts=3):
# Check and update must be atomic if multiple workers share the state.
if state.active or state.ready_at is None or mono_now < state.ready_at:
return False
if mono_now >= state.deadline_mono or state.attempts >= max_attempts:
state.ready_at = None
return False
state.active, state.ready_at = True, None
state.attempts += 1
return True # No HTTP request is made by this function.
Разбор Retry-After и конечный бюджет
Разбор значения должен различать как минимум четыре случая: заголовок отсутствует, содержит корректное delta-seconds, содержит корректный HTTP-date или не может быть разобран. Для delta-seconds проверяйте формат и не допускайте отрицательного результата. Для HTTP-date переводите дату в задержку относительно текущего wall-clock, затем используйте монотонное время для ожидания. Не следует превращать некорректный заголовок в бесконечный backoff или в немедленный бесконечный цикл. В этом примере некорректное значение приводит к stop; fallback_delay используется только при отсутствии заголовка. Парсер даты опирается на email.utils и не является строгим валидатором всего HTTP-поля; некорректные или неоднозначные даты следует отклонять по контракту клиента. В любом варианте должны действовать max_attempts и абсолютный deadline. Ограничение времени нужно считать частью контракта планировщика: если Retry-After указывает срок позже deadline, функция возвращает stop, а не ставит задачу в очередь. При повторных 429 можно применять отдельный предел общего времени ожидания; каждый новый ответ не должен продлевать его сверх исходного deadline.
from datetime import datetime, timezone
wall_now = datetime(2026, 11, 7, tzinfo=timezone.utc).timestamp()
state = RetryState(deadline_mono=200, attempts=1)
decision = plan_after_429(
state, "10", wall_now=wall_now, mono_now=100, safe_to_retry=True
)
assert decision.kind == "wait" and decision.ready_at == 110
assert not claim_due(state, mono_now=109)
assert claim_due(state, mono_now=110)
assert not claim_due(state, mono_now=110) # Already active.Параллельные повторы, платные операции и timeout
Один task_id должен иметь не более одной активной попытки и не более одного запланированного повтора. Это защищает от сценария, в котором несколько обработчиков одного 429 создают независимые таймеры и одновременно запускают одинаковую операцию. Для общего лимита сервиса может понадобиться отдельный глобальный rate limiter, но он не заменяет дедупликацию конкретной задачи. Автоповтор допустим только после подтверждения семантики операции и её политики ошибок. Наличие статуса 429 само по себе не доказывает, что повтор безопасен для платного POST, списания, создания ресурса или другой операции с побочными эффектами. Не предполагайте универсальную серверную идемпотентность; используйте подтверждённый контракт API, ключ идемпотентности или ручное решение, если это предусмотрено конкретным сервисом. Неопределённый timeout — отдельный случай. Если клиент не знает, получил ли сервер запрос и обработал ли его, такой результат нельзя автоматически превращать в retry-ветку для 429. Его следует передавать в отдельную процедуру reconciliation: проверка статуса операции, ключ идемпотентности, журнал результата или ручная проверка. Нельзя обещать, что ограничитель снимется через фиксированное время или что повтор разблокирует аккаунт.
Детерминированные fixtures и проверки
Тестовые fixtures должны моделировать только входы и решения локального scheduler, без сетевой отправки. Зафиксируйте wall_now и mono_now, чтобы результат не зависел от реальных часов. Минимальный набор проверок: - отсутствующий Retry-After использует явно заданную fallback-политику или возвращает stop/escalate; - delta-seconds больше max_wait останавливает задачу, не сокращая серверное ожидание; - HTTP-date преобразуется в задержку, а отрицательное значение не уходит в прошлое; - некорректное значение не создаёт бесконечное ожидание; - Retry-After за пределами deadline приводит к stop; - превышение max_attempts приводит к stop; - два события для одного active task_id не создают два повтора; - повторная постановка той же задачи не увеличивает число одновременных запусков; - неопределённый timeout не проходит через обработчик 429; - scheduler не вызывает сетевой клиент и возвращает только решение или запись в очередь. Отдельно проверяйте, что ограничение одного task_id не ошибочно блокирует независимые задачи. Глобальный бюджет попыток, бюджет конкретной задачи и ограничение параллелизма — разные политики, их состояния не следует смешивать.
Шаблон промпта
Подготовь русскоязычный технический материал о планировании повторов после HTTP 429 на основе RFC 6585 section 4 и RFC 9110 Retry-After. Покажи, что Retry-After может отсутствовать и задаваться как delta-seconds или HTTP-date. Объясни обработку отсутствующего, некорректного и слишком большого значения, рассинхронизацию часов, монотонные часы, абсолютный deadline, max_attempts и ограниченный бюджет ожидания. Отдели локальный bounded scheduler от сетевой отправки: приведи иллюстративный, явно нетестированный псевдокод или Python-подобный код без HTTP-вызовов. Покажи одну очередь, дедупликацию по task_id и запрет параллельных повторов одной задачи. Добавь детерминированные fixtures и проверки глобального или задачного бюджета попыток, отсутствия одновременных повторов и отделения timeout с неопределённым исходом от ветки 429. Не обещай фиксированное время восстановления, разблокировку аккаунта, универсальную идемпотентность или безопасность платных POST. Не выдумывай цены, benchmarks, результаты тестов, promotions, совместимость или дополнительные источники. Используй только заданные факты и явно отмечай ограничения.