← Блог

Ошибка 429: что такое rate limit и как правильно ретраить

· 7 минут чтения

Коротко

429 - это не ошибка вашего кода и не авария у вендора. Это сообщение «слишком часто, подожди». Проблема начинается тогда, когда код на него реагирует немедленным повтором: вы бьёте в закрытую дверь чаще, лимит не отпускает, и то, что было секундной паузой, превращается в лавину.

Что нужно знать по порядку:

  1. Лимитов несколько, и упереться можно в любой - запросы в минуту, токены в минуту, параллельные соединения.
  2. Ответ часто содержит Retry-After - точное число секунд. Его игнорируют чаще всего.
  3. Повтор должен быть с растущей задержкой и разбросом, иначе все ваши воркеры вернутся одновременно.
  4. Ретрай не лечит перегрузку - при устойчивом превышении нужна очередь, а не повторы.

Rate limit (лимит частоты) - ограничение на то, сколько запросов или токенов вы можете отправить за единицу времени. Оно есть у каждого вендора моделей и существует, чтобы один клиент не занял мощности всех остальных.

В какой именно лимит вы упёрлись

Одинаковый код 429 приходит на разные превышения, и лечатся они по-разному.

Схема из трех колонок, показывающая разницу между лимитами RPM, TPM и параллельными соединениями.
У каждого лимита — свой профиль нарушителя и свой способ лечения.

  • RPM, запросов в минуту. Упирается тот, кто шлёт много мелких запросов: классификация, извлечение полей, короткие ответы. Лечится склейкой - несколько объектов в один запрос.
  • TPM, токенов в минуту. Упирается тот, кто шлёт мало, но огромных запросов: длинный контекст, документы целиком. Лечится сокращением контекста, а не паузами.
  • Параллельные запросы. Упирается тот, у кого воркеров больше, чем разрешено соединений. Лечится семафором на своей стороне.
  • Дневная или месячная квота. Тут повторы бессмысленны в принципе: до сброса квоты ничего не изменится.

Первое, что стоит сделать при регулярных 429, - посмотреть в тело ответа. Вендоры пишут туда, какой именно лимит сработал, и это экономит день гаданий.

🔴 Отличайте 429 от 529 и 503. 429 - превышение вашего лимита, виноват профиль нагрузки. 529 и 503 - перегрузка на стороне вендора, ваш профиль ни при чём. Ретрай уместен в обоих случаях, но выводы разные: в первом надо править свою нагрузку, во втором - иметь запасную модель.

Retry-After: цифра, которую вам уже дали

Значительная часть ответов с 429 содержит заголовок Retry-After - сколько секунд ждать. Если он есть, гадать не нужно.

import time, random

def retry_after_seconds(err, attempt: int) -> float:
    headers = getattr(err, "response", None)
    headers = getattr(headers, "headers", {}) or {}
    raw = headers.get("retry-after")
    if raw:
        try:
            return float(raw)
        except ValueError:
            pass
    # заголовка нет - экспонента с разбросом
    return min(60.0, 2 ** attempt) * (0.5 + random.random())

Две вещи здесь важны одинаково.

Два графика на временной шкале, сравнивающие повторы запросов с постоянным интервалом и с экспоненциальным разбросом.
Без случайного разброса во времени ваши воркеры будут бесконечно создавать новые пики нагрузки.

Экспонента. Задержка растёт: 1, 2, 4, 8 секунд. Постоянная пауза в секунду при устойчивом превышении не даёт лимиту разжаться никогда.

Разброс (jitter). Множитель 0.5 + random() разносит повторы во времени. Без него двадцать воркеров, получивших 429 в одну секунду, вернутся тоже в одну секунду - и получат 429 снова. Это классическая лавина повторов, и ловится она ровно одной строкой со случайным числом.

Ретрай целиком

RETRIABLE = {429, 500, 502, 503, 529}

def ask(messages, *, model="claude-sonnet-5", attempts=5, **kw):
    for attempt in range(attempts):
        try:
            return client.chat.completions.create(
                model=model, messages=messages, **kw
            )
        except Exception as err:
            code = getattr(err, "status_code", None)
            if code not in RETRIABLE or attempt == attempts - 1:
                raise
            delay = retry_after_seconds(err, attempt)
            log.warning("%s, повтор через %.1f с (попытка %d)", code, delay, attempt + 1)
            time.sleep(delay)

Что тут сделано осознанно:

  • Список повторяемых кодов закрыт. 401 (неверный ключ), 400 (кривой запрос) и 402 (кончились деньги) не повторяются никогда - повтор их не вылечит, а лог засорит.
  • Попыток пять, не бесконечность. Бесконечный повтор превращает короткий сбой в вечно висящий запрос, который держит соединение и воркер.
  • Каждый повтор пишется в лог с кодом и задержкой. Без этого вы не узнаете, что половина запросов уходит со второй попытки, пока не придёт счёт за удвоенное время работы.

🔴 Повторы стоят денег и времени. Неудачный запрос, дошедший до модели, может успеть потратить токены. А пять попыток с экспонентой - это до минуты ожидания в пользовательском сценарии, где терпения секунд десять. Для интерактивных запросов ставьте две попытки и короткий потолок, а длинные цепочки оставьте фоновым задачам.

Когда ретрай не помогает

Повтор рассчитан на короткий всплеск. Если 429 приходит постоянно, вы не в всплеске, а в устойчивом превышении, и повторы только добавляют нагрузки.

Три рабочих выхода:

Ограничьте параллельность у себя. Семафор на количество одновременных запросов - самая дешёвая мера. Двадцать воркеров, каждый со своими повторами, дают лавину; пять воркеров с очередью дают ровный поток.

Визуализация потока данных, где семафор ограничивает количество одновременных задач до пяти.
Ретрай спасает от случайных всплесков, а от постоянной перегрузки спасет только очередь.

import asyncio
sem = asyncio.Semaphore(5)

async def ask_limited(messages, **kw):
    async with sem:
        return await ask_async(messages, **kw)

Склейте мелкие запросы. Если упираетесь в RPM, десять объектов в одном запросе - это десятикратный запас по лимиту при том же объёме токенов. Работает для классификации и извлечения, не работает для диалога.

Разведите нагрузку по моделям. Лимиты считаются на модель, а не на аккаунт целиком. Фоновая массовая обработка на лёгкой модели и интерактивные ответы на флагмане не мешают друг другу.

Что делает шлюз, а что остаётся вам

Часть работы снимает единый шлюз: при отказе провайдера запрос уходит на резервного, а несколько провайдеров одной модели дают суммарный запас по лимитам. Это заметно уменьшает частоту 429, но не отменяет ретрая.

Чего шлюз за вас не делает: не разбирает Retry-After, не ставит паузу в вашем коде и не ограничивает вашу параллельность. Рублёвый баланс и единый ключ упрощают оплату и переключение моделей - на профиль вашей нагрузки они не влияют никак. Ретрай и семафор пишутся на вашей стороне в любом случае.

С чего начать

  1. Залогируйте коды ответов за неделю. Пока вы не знаете свою долю 429, всё остальное - догадки.
  2. Прочитайте Retry-After и используйте его вместо своей константы.
  3. Добавьте экспоненту с разбросом - это две строки и самый большой эффект.
  4. Поставьте семафор на параллельные запросы, число подберите замером.
  5. Разделите интерактивную и фоновую нагрузку - по моделям и по числу попыток.

Все 44 модели с ценами и контекстом - на витрине. Разбор конкретных кодов Anthropic - в ошибках Claude API, а про запасную модель на случай отказа - в маршрутизации.

FAQ

Что означает ошибка 429 Too Many Requests?

Что вы превысили разрешённую частоту обращений: слишком много запросов, слишком много токенов за минуту или слишком много одновременных соединений. Это штатный ответ сервиса, а не поломка вашего кода и не авария у вендора.

Как правильно повторять запрос после 429?

С растущей задержкой и случайным разбросом: 1, 2, 4, 8 секунд, каждая умноженная на случайный множитель. Если в ответе есть заголовок Retry-After, брать паузу из него. Повторять сразу или через фиксированную секунду - худший вариант, он поддерживает превышение.

Чем 429 отличается от 529?

429 говорит, что превышен ваш личный лимит - надо править свой профиль нагрузки. 529 (и 503) говорят, что перегружен сам вендор, и ваша нагрузка ни при чём. Повторять стоит оба, но во втором случае помогает переключение на другую модель, а не пауза.

Почему после добавления ретрая стало хуже?

Скорее всего, повторы уходят без разброса: все воркеры получили 429 одновременно и одновременно же вернулись. Плюс каждый повтор - это дополнительный запрос к тому же лимиту. Лечится случайным множителем в задержке и ограничением параллельности.

Как понять, в какой лимит я упёрся?

По телу ответа: вендоры указывают, какой лимит сработал - запросы в минуту, токены в минуту или параллельные соединения. Косвенно - по профилю: много мелких запросов означает упор в RPM, мало больших - в TPM.

Убирает ли агрегатор ошибки 429?

Уменьшает, но не убирает. Несколько провайдеров одной модели дают суммарный запас по лимитам и подмену при отказе. Но Retry-After за вас никто не прочитает и параллельность вашего кода не ограничит - ретрай нужен всё равно.