Лимиты запросов Противодавление Надёжность LLM API

Лимиты запросов LLM API и надёжное противодавление

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

AveMujica API 8 мин чтения

Когда провайдер LLM ограничивает вас по частоте, он возвращает HTTP 429, и наивная реакция — повторить запрос немедленно — только усугубляет ситуацию, потому что неуспешные запросы всё равно засчитываются в ваш поминутный лимит. Долговечное решение — это два слоя, работающих вместе: читать заголовки лимитов, которые присылает провайдер, чтобы знать, сколько бюджета осталось, и применять противодавление на своей стороне — ограниченные очереди, потолки параллелизма и backoff с джиттером — так, чтобы, когда спрос превышает предложение, ваша система замедлялась, а не плавилась. В этой статье рассматриваются оба слоя и код для их реализации.

Лимиты запросов — это не ошибочное состояние, которое нужно замазать; это сигнал о ёмкости. Система, которая относится к ним как к сигналу, деградирует предсказуемо. Система, которая их игнорирует, колеблется между тем, чтобы молотить провайдера, и тем, чтобы отбрасывать запросы пользователей.

Прочитайте заголовки, прежде чем что-либо настраивать

OpenAI возвращает состояние лимитов в каждом ответе через заголовки, которые вы можете инспектировать напрямую. Набор на основе запросов таков:

  • x-ratelimit-limit-requests — потолок запросов в окне
  • x-ratelimit-remaining-requests — сколько у вас осталось
  • x-ratelimit-reset-requests — время до сброса окна запросов

Есть параллельный набор на основе токенов — x-ratelimit-limit-tokens, x-ratelimit-remaining-tokens, x-ratelimit-reset-tokens — и варианты в масштабе проекта, такие как x-ratelimit-remaining-project-tokens. Большинство реальных лимитов срабатывает по токенам, а не по числу запросов, потому что один вызов с длинным контекстом может съесть большой кусок вашего токенового бюджета. Логируйте эти заголовки в каждом ответе — и вы увидите приближение троттлинга ещё до того, как он превратится в стену из 429:

resp = client.chat.completions.with_raw_response.create(
    model="gpt-5.6",
    messages=[{"role": "user", "content": prompt}],
)
h = resp.headers
print(h.get("x-ratelimit-remaining-requests"), h.get("x-ratelimit-remaining-tokens"))

Логирование этих заголовков — операционная основа: записывая x-ratelimit-remaining-requests и x-ratelimit-remaining-tokens для каждого ответа, вы увидите, какой апстрим приближается к своему потолку ещё до начала 429. Шлюз хорошо подходит для централизации такого логирования, поскольку каждый ответ уже проходит через него. Но кривую оставшегося бюджета по каналам вы строите поверх сырых заголовков сами — ответ не отдаёт её в заранее вычисленном виде.

Повторяйте 429 с экспоненциальным backoff и джиттером

Когда 429 всё же приходит, отступите перед повтором. Экспоненциальный backoff умножает ожидание после каждой неудачи; джиттер рандомизирует его, чтобы флот клиентов не повторял всё разом в один и тот же тик и не сталкивался снова — это эффект «стада» (thundering herd), который воссоздаёт ту самую перегрузку, из которой вы выбираетесь.

import random, time

def backoff_sleep(attempt, base=0.5, cap=20.0):
    # full jitter: random point in [0, min(cap, base * 2**attempt))
    ceiling = min(cap, base * (2 ** attempt))
    time.sleep(random.uniform(0, ceiling))

for attempt in range(6):
    r = call_model(prompt)
    if r.status_code != 429:
        break
    backoff_sleep(attempt)

Два правила удерживают это в честных рамках. Первое: ограничьте и задержку, и число попыток — бесконечные повторы лишь переносят сбой ниже по потоку. Второе: повторяйте только то, что подлежит повтору: 429 и временные 5xx/таймауты повторять стоит; а 400 из-за некорректного запроса будет одинаково падать каждый раз. Поскольку повторённая запись может продублировать работу, сочетайте повторы с идемпотентностью — полный разбор в Повторы и идемпотентность LLM API.

Если в ответе есть заголовок Retry-After, предпочтите его вашему вычисленному backoff — сервер точно говорит вам, когда окно откроется снова, и соблюдение этого значения и восстанавливает быстрее, и избавляет от заведомо обречённых попыток. Считайте свой экспоненциальный график запасным вариантом на случай, когда сервер не даёт явной подсказки.

Противодавление: замедлите источник, не роняйте пол

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

Три механизма складываются в стратегию противодавления:

МеханизмЧто он ограничиваетКакой режим отказа предотвращает
Ограничение параллелизма (семафор)Запросы в полётеОткрытие тысяч сокетов, каждый из которых даёт 429
Ограниченная очередьРаботу, ждущую стартаБезграничный рост памяти при всплеске
Контроль допуска / сброс нагрузкиНовую принимаемую работуПриём запросов, которые вы никогда не успеете обслужить

Ограничение параллелизма — это изменение с наибольшим рычагом и самое простое в добавлении. Ограничьте одновременные вызовы в полёте, чтобы соответствовать, а не превышать терпимость провайдера:

import asyncio

sem = asyncio.Semaphore(20)  # never more than 20 concurrent upstream calls

async def guarded_call(prompt):
    async with sem:
        return await call_model_async(prompt)

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

Сбрасывайте нагрузку намеренно, когда очередь полна

Ограниченная очередь в конце концов заполняется при устойчивом всплеске. Что происходит потом — это продуктовое решение, и сделать его явным — вот в чём весь смысл:

  • Быстро отклонить — вернуть 503 с Retry-After, чтобы вызывающие повторили позже. Лучше всего для интерактивного трафика, где медленный ответ хуже, чем ясное «попробуйте снова».
  • Сбрасывать по приоритету — отбрасывать или откладывать низкоприоритетную работу (пакетную, фоновую), чтобы интерактивные запросы продолжали идти.
  • Деградировать — направлять переполнение на более дешёвую и быструю модель, у которой скорее есть запас.

Антипаттерн — это безграничная очередь, которая принимает всё и тихо раздувает задержку, пока запросы всё равно не истекут по таймауту. Это превращает видимый, восстановимый отказ в невидимый общесистемный застой. Ограничьте очередь и решите, что означает «выпасть с хвоста».

Где шлюз централизует политику

Противодавление, реализованное в каждом сервисе, дрейфует: каждый выбирает свои лимиты, и они плохо взаимодействуют. Шлюз — естественное место для применения единой согласованной политики, потому что весь трафик и так проходит через него. В этом проекте соединения с провайдерами регистрируются как каналы, у каждого своя квота и приоритет; шлюз уже переключается на здоровый канал, когда один начинает троттлить (стратегия отказоустойчивости LLM API), и записывает использование по каждому запросу, на основе которого можно настраивать систему. Более развитый механизм допуска — это архитектура, которую вы добавляете в той же точке концентрации:

  • отслеживать оставшийся токеновый и запросный бюджет по каждому каналу из заголовков ответа,
  • применять поканальные ограничения параллелизма, чтобы один «горячий» канал не морил голодом другие,
  • держать единый совокупный бюджет повторов, чтобы восстановительный трафик не усиливал сбой.

Считайте эти три пункта проектом для реализации на шлюзе, а не переключателями, которые доступны по умолчанию; ценность шлюза в том, что место для их добавления одно. У централизации есть конкретная причина: если десять сервисов независимо повторяют по 429, их суммарный трафик повторов сам по себе может превысить ваш лимит. Единый общий бюджет повторов предотвращает это самонанесённое усиление.

Многослойная оборона, в порядке влияния

Применяйте их по порядку; каждый выигрывает время для следующего:

  1. Измеряйте — логируйте заголовки лимитов, чтобы троттлинг был виден рано.
  2. Ограничьте параллелизм — семафор, соответствующий терпимости провайдера.
  3. Ограничьте очередь — фиксированный буфер, никогда не безграничный.
  4. Отступайте с джиттером — на 429/5xx, никогда на клиентских ошибках 4xx.
  5. Сбрасывайте или деградируйте — явная политика для полной очереди.
  6. Распределяйте нагрузку — несколько каналов и отказоустойчивость, чтобы поднять эффективный потолок.

Первые два изменения обычно устраняют большую часть боли от 429. Остальное превращает оставшиеся всплески в плавные замедления вместо простоев.

Бюджетируйте токены, а не только запросы

Поскольку токеновые лимиты обычно упираются раньше запросных, оцените токеновую стоимость вызова до отправки и зарезервируйте бюджет соответственно. Запрос потребляет и входные токены (ваш промпт плюс любую сцепленную историю), и выходные (ограниченные max_tokens/max_output_tokens). Если вы допускаете работу только по числу запросов, всплеск вызовов с длинным контекстом может пробить токеновый потолок, пока счётчик запросов выглядит здоровым, — и каждый такой вызов вернётся с 429.

Практическое правило допуска — это токеночувствительный шлюз: отслеживайте оставшийся токеновый бюджет из x-ratelimit-remaining-tokens, оценивайте худший случай по токенам для каждого ожидающего вызова и допускайте только те вызовы, чья оценка укладывается в оставшийся запас. Это идея token bucket, применённая к LLM-трафику — пополнять на интервале сброса, тратить при допуске и заставлять вызывающих ждать, когда ведро на исходе.

  • Ограничивайте max_tokens/max_output_tokens до размера, который вы действительно ожидаете; завышенный потолок резервирует бюджет, который вы никогда не используете, и морит голодом другие вызовы.
  • Считайте сцепленную историю: при сохранённом состоянии диалога прежние входные токены всё равно тарифицируются как входные, поэтому длинная ветка обходится дороже за ход, чем кажется.
  • Предпочитайте много маленьких вызовов одному гигантскому, когда единственный запрос зарезервировал бы большой кусок токенового окна разом.

Правильный размер токеновой оценки часто больший рычаг, чем настройка параллелизма, потому что он не даёт вызовам, которые дали бы 429, вообще быть допущенными. Стоимость и ёмкость здесь — один и тот же регулятор, поэтому это связано со снижением затрат на LLM API.

Часто задаваемые вопросы

Какой код статуса означает, что меня ограничивают по частоте, и стоит ли его повторять?

HTTP 429. Он подлежит повтору, но только с backoff и джиттером, никогда немедленно — неуспешные запросы всё равно засчитываются в ваш поминутный лимит, поэтому тесные циклы повторов углубляют троттлинг. Ограничьте попытки и задержку.

На что я реально наткнусь — на лимиты запросов или токенов?

Обычно на токеновые лимиты. Проверяйте x-ratelimit-remaining-tokens вместе с x-ratelimit-remaining-requests. Нагрузка с длинными промптами или большим max_tokens может исчерпать токеновый бюджет, едва задев счётчик запросов, поэтому бюджетирование только по числу запросов вводит в заблуждение.

Повышение лимита параллелизма увеличивает пропускную способность?

Только вплоть до терпимости провайдера. За этой точкой лишний параллелизм даёт больше 429, больше повторов и меньшую полезную пропускную способность. Настройте семафор на наибольшее значение, при котором частота 429 остаётся около нуля, и остановитесь.

Очередь побольше — это лекарство от устойчивой перегрузки?

Нет. Бо́льшая очередь лишь оттягивает момент истины и раздувает хвостовую задержку, пока заполняется. Если приём устойчиво превышает ёмкость, никакой буфер вас не спасёт — нужны сброс нагрузки, более высокий лимит или больше каналов. Очереди поглощают всплески, а не тренды.

Начните с одного семафора и логирования заголовков

Добавьте один семафор параллелизма вокруг вызовов модели и начните логировать x-ratelimit-remaining-tokens в каждом ответе. Сопоставьте записанные значения с 429 за один день: если провалы остатка токенов совпадают с ошибками, понижайте семафор, пока провалы не прекратятся. Одна эта петля обратной связи устраняет большинство инцидентов с лимитами ещё до того, как вы построите что-то более сложное.

Ссылки