429 Retry-After: Seconds or Date — Checking the API Client's Wait Time
Practical analysis of two Retry-After formats for HTTP 429: parsing seconds and HTTP dates, limiting wait time, and testing six boundary cases in Python 3.12.

In this article
Если API ответил 429 и прислал Retry-After, клиенту нужно распознать две допустимые формы: целое число секунд или HTTP-дату. Число задаёт относительную паузу, а из даты вычисляют неотрицательную разницу со временем ответа. Некорректное значение лучше не угадывать: применить заранее заданную резервную политику и записать причину.
Что именно обещает Retry-After
Код 429 означает, что клиент отправил слишком много запросов за некоторый интервал. Ответ может содержать Retry-After с подсказкой, сколько ждать; это описано в RFC 6585.
Синтаксис поля задаёт RFC 9110: либо HTTP-date, либо delay-seconds — неотрицательное десятичное число секунд. Базовая грамматика RFC 5234 определяет DIGIT как ASCII-цифры 0–9; метод isdecimal() без isascii() шире этого правила. Поэтому 120 и Sun, 11 Oct 2026 05:02:00 GMT требуют разных веток разбора.
На маленьком примере проверим только вычисление паузы. Это учебная модель клиента, а не тест конкретного API 1С-Битрикс. Среда: CPython 3.12.14, стандартная библиотека, UTC; дата проверки — 14 октября 2026 года.
Минимальная функция
У функции три входа: значение поля, время ответа и локальный предел ожидания. Предел 300 секунд выбран для учебного примера; для рабочего клиента его определяют отдельно.
from datetime import datetime
from email import utils
def retry_after_seconds(value: str, response_date: datetime, max_wait: int = 300) -> int:
value = value.strip()
if value.isascii() and value.isdecimal():
delay = int(value)
else:
retry_at = utils.parsedate_to_datetime(value)
if retry_at.tzinfo is None:
raise ValueError("Retry-After date must include a timezone")
delay = max(0, int((retry_at - response_date).total_seconds()))
return min(delay, max_wait)
Функцию parsedate_to_datetime() предоставляет стандартная библиотека Python. При успешном разборе она возвращает datetime, а для некорректных значений даты выбрасывает ValueError. Проверка tzinfo не позволяет молча принять дату без часового пояса. Подробности синтаксиса есть в документации Python.
Почему нужен response_date
Абсолютная дата должна превратиться в длительность. Если ответ содержит поле Date, удобно считать от него: оба значения сформированы серверной стороной, поэтому возможное расхождение между часами клиента и сервера меньше влияет на расчёт. Это инженерная мера, а не отдельное правило Retry-After. Без Date явно используют локальное время получения ответа и учитывают возможное расхождение часов клиента и сервера.
Прошедшая HTTP-дата даёт нулевую паузу, а не отрицательную. Но ноль не означает «повторять бесконечно»: следующий шаг всё равно ограничивают общим бюджетом попыток.
Воспроизводимая проверка
Фиксируем время ответа и шесть случаев: обе формы, определённые стандартом, прошедшую дату, чрезмерную задержку, ошибочную строку и десятичные Unicode-цифры вне ASCII.
from datetime import datetime, timezone
REFERENCE = datetime(2026, 10, 11, 5, 0, tzinfo=timezone.utc)
CASES = [
("120", 120),
("Sun, 11 Oct 2026 05:02:00 GMT", 120),
("Sun, 11 Oct 2026 04:59:00 GMT", 0),
("86400", 300),
]
for raw, expected in CASES:
actual = retry_after_seconds(raw, REFERENCE)
assert actual == expected, (raw, actual, expected)
print(f"{raw!r} -> {actual} s")
try:
retry_after_seconds("two minutes", REFERENCE)
except ValueError as exc:
print(f"'two minutes' -> {type(exc).__name__}")
else:
raise AssertionError("invalid value was accepted")
unicode_digits = chr(0x0661) + chr(0x0662) + chr(0x0660)
try:
retry_after_seconds(unicode_digits, REFERENCE)
except ValueError as exc:
print(f"'U+0661 U+0662 U+0660' -> {type(exc).__name__}")
else:
raise AssertionError("Unicode digits outside ASCII were accepted")
Запуск завершился с кодом 0. Наблюдаемый вывод:
'120' -> 120 s
'Sun, 11 Oct 2026 05:02:00 GMT' -> 120 s
'Sun, 11 Oct 2026 04:59:00 GMT' -> 0 s
'86400' -> 300 s
'two minutes' -> ValueError
'U+0661 U+0662 U+0660' -> ValueError
Вход | Ожидание | Результат | Что подтверждает |
|---|---|---|---|
120 | 120 с | 120 с | Относительная задержка |
HTTP-дата +120 с | 120 с | 120 с | Абсолютная дата |
HTTP-дата −60 с | 0 с | 0 с | Нет отрицательной паузы |
86400 при max_wait=300 | 300 с | 300 с | Локальный предел |
two minutes | Ошибка | ValueError | Недопустимое значение отклонено |
U+0661 U+0662 U+0660 | Ошибка | ValueError | Unicode-цифры вне ASCII отклонены |
Что делать с пустым или неправильным заголовком
Не подставлять ноль: немедленный повтор может продлить ограничение.
Не превращать произвольную строку в большое число: ошибку разбора наблюдают отдельно.
Применить документированную резервную задержку с jitter и общий предел попыток.
Сохранить статус, Retry-After и номер попытки без токенов и персональных данных.
Остановить повтор, если операция не защищена от дублей.
Тут есть подвох: корректная пауза не делает POST безопасным. После ожидания соединение снова может оборваться уже после обработки запроса сервером. Для создания заказа нужен идемпотентный ключ или другой механизм дедупликации.
Отдельный проверяемый пример разобран в статье «Повторный webhook не должен создать второй заказ». Retry-After отвечает за момент следующей попытки, а идемпотентность — за её последствия.
Границы решения
Пример не отправляет реальные запросы и не измеряет лимиты конкретного сервиса.
Retry-After может отсутствовать: тогда действует собственная заранее описанная политика клиента.
Максимум 300 секунд выбран только для теста; протокол такого потолка не задаёт.
Нескольким воркерам нужен общий ограничитель или координация, иначе они проснутся одновременно.
Пауза должна быть прерываемой при остановке задания.
Критерий приёмки клиента
Клиент готов к 429, если одинаково обрабатывает число и HTTP-дату, не допускает отрицательной задержки, не принимает мусор, ограничивает ожидание и количество попыток, а повторяемая операция защищена от дубля. По одному успешному повтору выводить рано: нужны проверки веток ошибки и предельных значений.
If the API returns 429 and includes Retry-After, the client must recognize two valid formats: an integer number of seconds or an HTTP date. The number specifies a relative wait time, while the date is used to calculate a non-negative difference from the response time. Do not guess incorrect values; instead, apply a predefined fallback policy and log the reason.
What Retry-After Specifically Promises
Status code 429 indicates that the client sent too many requests within a certain interval. The response may include a Retry-After header with a hint on how long to wait; this is described in RFC 6585.
The field syntax is defined by RFC 9110: either an HTTP-date or delay-seconds, a non-negative decimal number of seconds. The base grammar RFC 5234 defines DIGIT as ASCII digits 0–9; the isdecimal() method without isascii() is broader than this rule. Therefore, 120 and Sun, 11 Oct 2026 05:02:00 GMT require different parsing branches.
In a small example, we will verify only the wait time calculation. This is a learning model for a client, not a test of a specific 1C-Bitrix API. Environment: CPython 3.12.14, standard library, UTC; check date: October 14, 2026.
Minimal Function
The function has three inputs: the field value, the response time, and a local wait limit. The limit of 300 seconds is chosen for this learning example; for a production client, it is determined separately.
from datetime import datetime
from email import utils
def retry_after_seconds(value: str, response_date: datetime, max_wait: int = 300) -> int:
value = value.strip()
if value.isascii() and value.isdecimal():
delay = int(value)
else:
retry_at = utils.parsedate_to_datetime(value)
if retry_at.tzinfo is None:
raise ValueError("Retry-After date must include a timezone")
delay = max(0, int((retry_at - response_date).total_seconds()))
return min(delay, max_wait)
The parsedate_to_datetime() function is provided by the standard Python library. On successful parsing, it returns a datetime object; for invalid date values, it raises a ValueError. Checking tzinfo prevents silently accepting a date without a time zone. Syntax details are available in the Python documentation.
Why response_date is needed
An absolute date must be converted into a duration. If the response contains a Date field, it is convenient to calculate from it: both values are generated server-side, so the possible discrepancy between client and server clocks has less impact on the calculation. This is an engineering measure, not a separate Retry-After rule. Without an explicit Date, the local time of receiving the response is used, taking into account the possible clock discrepancy between the client and the server.
A past HTTP date results in a zero delay, not a negative one. However, zero does not mean "retry infinitely": the next step is still limited by the total attempt budget.
Reproducible check
We record the response time and six cases: both forms defined by the standard, a past date, excessive delay, an invalid string, and decimal Unicode digits outside ASCII.
from datetime import datetime, timezone
REFERENCE = datetime(2026, 10, 11, 5, 0, tzinfo=timezone.utc)
CASES = [
("120", 120),
("Sun, 11 Oct 2026 05:02:00 GMT", 120),
("Sun, 11 Oct 2026 04:59:00 GMT", 0),
("86400", 300),
]
for raw, expected in CASES:
actual = retry_after_seconds(raw, REFERENCE)
assert actual == expected, (raw, actual, expected)
print(f"{raw!r} -> {actual} s")
try:
retry_after_seconds("two minutes", REFERENCE)
except ValueError as exc:
print(f"'two minutes' -> {type(exc).__name__}")
else:
raise AssertionError("invalid value was accepted")
unicode_digits = chr(0x0661) + chr(0x0662) + chr(0x0660)
try:
retry_after_seconds(unicode_digits, REFERENCE)
except ValueError as exc:
print(f"'U+0661 U+0662 U+0660' -> {type(exc).__name__}")
else:
raise AssertionError("Unicode digits outside ASCII were accepted")
The run completed with code 0. Observed output:
'120' -> 120 s
'Sun, 11 Oct 2026 05:02:00 GMT' -> 120 s
'Sun, 11 Oct 2026 04:59:00 GMT' -> 0 s
'86400' -> 300 s
'two minutes' -> ValueError
'U+0661 U+0662 U+0660' -> ValueError
Input | Expectation | Result | What it confirms |
|---|---|---|---|
120 | 120 s | 120 s | Relative delay |
HTTP date +120 s | 120 s | 120 s | Absolute date |
HTTP date −60 s | 0 s | 0 s | No negative delay |
86400 with max_wait=300 | 300 s | 300 s | Local limit |
two minutes | Error | ValueError | Invalid value rejected |
U+0661 U+0662 U+0660 | Error | ValueError | Unicode digits outside ASCII rejected |
What to do with an empty or incorrect header
Do not substitute zero: an immediate retry may extend the limit.
Do not convert arbitrary strings to large numbers: parse errors are handled separately.
Apply the documented backup delay with jitter and a total attempt limit.
Preserve status, Retry-After, and attempt number without tokens or personal data.
Stop the retry if the operation is not protected against duplicates.
There is a catch here: a correct delay does not make a POST request safe. After waiting, the connection may still break after the server has already processed the request. To create an order, an idempotent key or another deduplication mechanism is required.
A separate, verifiable example is analyzed in the article "A repeated webhook must not create a second order". Retry-After governs the timing of the next attempt, while idempotency governs its consequences.
Solution Boundaries
The example does not send real requests and does not measure the limits of a specific service.
Retry-After may be absent: in that case, the client's pre-defined policy applies.
A maximum of 300 seconds was chosen only for testing; the protocol does not define such a ceiling.
Multiple workers require a shared limiter or coordination, otherwise they will wake up simultaneously.
The pause must be interruptible upon stopping the job.
Client Acceptance Criteria
A client is ready for 429 if it handles numbers and HTTP dates identically, does not allow negative delays, rejects garbage, limits wait time and retry counts, and protects repeatable operations against duplicates. It is premature to rely on a single successful retry: error branches and boundary values must also be verified.





Discussion 0
Share your experience and ask questions. Comments without links appear after editorial review.
No comments yet. Start the discussion.