Бонусы за приглашения

Как работают бонусы за приглашения

Поделитесь ссылкой. Когда друг зарегистрируется по ней и пополнит баланс, вы получите указанный бонус за его последующие пополнения.

Ошибка API 429: как настроить безопасный Retry-After и экспоненциальный backoff

Практическое руководство по устранению ошибки API 429: разбор лимитов RPM и TPM, чтение заголовка Retry-After и реализация Full Jitter Exponential Backoff в коде.

Содержание

Ошибка API 429: как настроить безопасный Retry-After и экспоненциальный backoff

Ошибка HTTP 429 Too Many Requests возникает, когда клиент превышает установленные провайдером лимиты частоты запросов или объема токенов. Неконтролируемый бесконечный retry не решает проблему, а лишь усугубляет блокировку, приводя к эффекту «шторма повторов» (retry storm).

Чтобы восстановить стабильную работу приложения, необходимо точно определить тип ограничения (RPM, TPM или исчерпание баланса), корректно распарсить заголовок Retry-After и внедрить алгоритм экспоненциального отката со случайным шумом (Full Jitter Exponential Backoff).

Почему возникает HTTP 429: анатомия лимитов

В современных LLM API (OpenAI, Anthropic и совместимых шлюзах) статус 429 генерируется тремя принципиально разными механизмами:

  1. RPM (Requests Per Minute) — ограничение на количество HTTP-обращений в минуту. Возникает при параллельном запуске нескольких воркеров или циклов без очередей.
  2. TPM (Tokens Per Minute) — ограничение на суммарный объем входных и выходных токенов за скользящее минутное окно. Часто срабатывает при отправке длинного контекста или больших системных инструкций.
  3. Исчерпание квоты / баланса (Quota / Balance Exhausted) — блокировка из-за нулевого депозита, достижения жесткого лимита расходов (spend limit) или исчерпания предоплаченного пакета.
HTTP/1.1 429 Too Many Requests
Date: Sun, 23 Aug 2026 03:00:00 GMT
Content-Type: application/json
Retry-After: 6
x-ratelimit-limit-requests: 500
x-ratelimit-remaining-requests: 0
x-ratelimit-reset-requests: 6s
x-ratelimit-limit-tokens: 30000
x-ratelimit-remaining-tokens: 1200
x-ratelimit-reset-tokens: 150ms

{
  "error": {
    "message": "Rate limit reached for model in organization on tokens per minute (TPM). Please try again in 6s.",
    "type": "tokens",
    "param": null,
    "code": "rate_limit_exceeded"
  }
}

Если ошибка вызвана исчерпанием баланса или жестким лимитом подписки, повторные запросы бессмысленны — они только тратят ресурсы сети. Чтобы сразу изолировать причину сбоя и не гадать по кодам в логах, в BetterToken предусмотрен прозрачный Dashboard: в нем в реальном времени отображаются HTTP-статусы каждого запроса, точное распределение Input/Output/Cache токенов и остаток баланса по модели pay-as-you-go без внезапных 5-часовых блокировок подписки.

Диагностическая матрица ошибки 429

СимптомПричинаЧто проверить в ответеИнженерное решение
Сбой при параллельных вызовахПревышен RPMx-ratelimit-remaining-requests: 0Ограничить параллелизм через семафор, внедрить очередь
Сбой на длинных промптахПревышен TPMx-ratelimit-remaining-tokens < размер промптаРазбить запрос, оптимизировать системный контекст, включить prompt caching
429 на 100% запросовБаланс / КвотаКод insufficient_quota или сообщение о billingОстановить retry, пополнить баланс или проверить права ключа
Лавинообразный рост 429Retry StormЗаголовки Retry-After игнорируются воркерамиДобавить случайный джиттер (Jitter) к задержке

Как правильно читать заголовок Retry-After

Спецификация RFC 6585 предусматривает передачу времени ожидания в заголовке Retry-After в двух форматах:

  • Число секунд (целое число или дробное значение, например Retry-After: 12);
  • Дата по стандарту HTTP-date (например, Retry-After: Sun, 23 Aug 2026 03:05:00 GMT).
import datetime
import email.utils
import time

def parse_retry_after(header_value: str | None, default_delay: float = 1.0) -> float:
    if not header_value:
        return default_delay
    header_value = header_value.strip()
    try:
        # Попытка парсинга секунд (float/int)
        return max(0.0, float(header_value))
    except ValueError:
        pass

    try:
        # Попытка парсинга HTTP-date
        parsed_date = email.utils.parsedate_to_datetime(header_value)
        now = datetime.datetime.now(datetime.timezone.utc)
        delay = (parsed_date - now).total_seconds()
        return max(0.0, delay)
    except Exception:
        return default_delay

Реализация Full Jitter Exponential Backoff

Если заголовок Retry-After отсутствует, стандартным решением является экспоненциальный откат (Exponential Backoff) с добавлением случайного шума (Full Jitter). Формула вычисления интервала для попытки $i$:

$$T_{\text{wait}} = \text{random}(0, \min(T_{\text{max}}, T_{\text{base}} \times 2^i))$$

Это предотвращает одновременную отправку повторных запросов всеми потоками после истечения тайм-аута.

import asyncio
import json
import random
import httpx

class RateLimitRetryClient:
    def __init__(
        self,
        base_url: str = "https://www.bettertoken.ai/v1",
        api_key: str = "",
        max_retries: int = 4,
        base_delay: float = 1.0,
        max_delay: float = 32.0,
    ):
        self.base_url = base_url
        self.api_key = api_key
        self.max_retries = max_retries
        self.base_delay = base_delay
        self.max_delay = max_delay
        self.client = httpx.AsyncClient(
            base_url=self.base_url,
            headers={"Authorization": f"Bearer {self.api_key}"},
            timeout=60.0,
        )

    async def send_chat_completion(self, payload: dict) -> dict:
        for attempt in range(self.max_retries + 1):
            try:
                response = await self.client.post("/chat/completions", json=payload)
                
                if response.status_code == 200:
                    return response.json()

                if response.status_code == 429:
                    error_data = response.json().get("error", {})
                    error_code = error_data.get("code")

                    # Если квота исчерпана полностью, повторы бесполезны
                    if error_code in ("insufficient_quota", "billing_not_active"):
                        raise RuntimeError(f"Критическая ошибка биллинга: {error_data.get('message')}")

                    if attempt == self.max_retries:
                        raise RuntimeError(f"Превышен лимит попыток (429): {response.text}")

                    # Определяем время ожидания
                    retry_after = response.headers.get("Retry-After")
                    if retry_after:
                        wait_time = parse_retry_after(retry_after) + random.uniform(0.1, 0.5)
                    else:
                        # Full Jitter Exponential Backoff
                        backoff_cap = min(self.max_delay, self.base_delay * (2 ** attempt))
                        wait_time = random.uniform(0, backoff_cap)

                    await asyncio.sleep(wait_time)
                    continue

                # Обработка других HTTP-ошибок
                response.raise_for_status()

            except httpx.RequestError as exc:
                if attempt == self.max_retries:
                    raise
                wait_time = min(self.max_delay, self.base_delay * (2 ** attempt))
                await asyncio.sleep(wait_time)

        raise RuntimeError("Не удалось выполнить запрос: исчерпан лимит повторов")

Безопасность и идемпотентность при повторных запросах

Повторять идемпотентные запросы на чтение данных (GET) безопасно. Однако при работе с генеративными моделями через POST:

  1. Исключите дублирование задач: если запрос прервался по таймауту после отправки, не повторяйте его немедленно без проверки факта списания или статуса выполнения.
  2. Используйте клиентские идентификаторы (Client Request ID): передавайте уникальный заголовок X-Request-ID для сквозного аудита в логах.
  3. Не маскируйте 401 и 403 под 429: ошибки авторизации требуют исправления ключа, а не паузы в цикле.

Контрольная проверка и валидация восстановления

Чтобы убедиться, что система вышла из режима ограничения лимитов:

  • Отправьте одиночный probe-запрос с минимальным промптом (max_tokens: 5).
  • Убедитесь в получении HTTP 200 и проверьте заголовок x-ratelimit-remaining-requests.
  • Постепенно восстанавливайте параллельный поток задач, контролируя долю ошибок 429 в метриках.

Чтобы исключить неожиданные 429 ошибки из-за жестких минутных лимитов и прозрачно отслеживать статус каждого запроса, перейдите на BetterToken API, создайте изолированный ключ и контролируйте расход токенов в реальном времени через Dashboard.

Источники и спецификации

Готовы оптимизировать LLM workflow?

Подключите единый API, управляйте ключами и контролируйте расходы на AI-модели в BetterToken.

Начать бесплатно