Проблема: провайдер считает твои запросы
Любой LLM-провайдер ставит лимиты на то, как быстро ты можешь к нему обращаться, — чтобы защитить свою инфраструктуру и справедливо поделить мощности между клиентами. Превысил — получаешь 429 Too Many Requests. У LLM лимиты обычно двумерные:
- RPM (requests per minute) — сколько запросов в минуту.
- TPM (tokens per minute) — сколько токенов в минуту (вход + выход). Часто именно TPM упирается первым, ведь один запрос агента может весить десятки тысяч токенов.
- Иногда — лимиты на параллельные запросы, дневные квоты, бюджет.
Специфика агентов делает лимиты особенно болезненными: агент на один запрос пользователя делает много вызовов LLM (reasoning-цикл, вызовы инструментов, суммаризация). Десять параллельных пользователей → десятки одновременных вызовов → мгновенно упёрлись в TPM. И тут — ключевая ошибка новичка:
Если на 429 ты просто повторяешь запрос (даже с backoff), ты снова бьёшься в тот же лимит — а рядом копятся ещё запросы, тоже получающие 429 и тоже ретраящие. Очередь растёт, латентность взлетает, часть запросов вообще не проходит. 429 — это не «сервер моргнул», а «ты едешь слишком быстро». Лечение не в повторах, а в том, чтобы изначально не превышать темп. Backoff при 429 нужен (уважай Retry-After), но это подстраховка, а не решение.
Клиентский rate limiter
Идея: поставить ограничитель на своей стороне, который пропускает запросы к провайдеру не быстрее заданного темпа (чуть ниже лимита провайдера). Тогда 429 просто не возникает — запросы ждут своей очереди у тебя, а не отскакивают от провайдера.
Классический алгоритм — token bucket («корзина токенов»). Интуиция простая:
Корзина пополняется токенами с постоянной скоростью R (это твой целевой темп). Каждый запрос «забирает» токен: есть токен — проходит сразу; нет — ждёт, пока корзина пополнится. Ёмкость корзины задаёт допустимый всплеск (burst) — можно накопить и пропустить пачку разом. Средний темп при этом никогда не превышает R.
import time, threading
class TokenBucket:
def __init__(self, rate: float, capacity: float):
self.rate = rate # токенов/сек (чуть НИЖЕ лимита провайдера!)
self.capacity = capacity # burst
self.tokens = capacity
self.updated = time.monotonic()
self.lock = threading.Lock()
def acquire(self, need: float = 1.0):
while True:
with self.lock:
now = time.monotonic()
self.tokens = min(self.capacity,
self.tokens + (now - self.updated) * self.rate)
self.updated = now
if self.tokens >= need:
self.tokens -= need
return # пропускаем запрос
wait = (need - self.tokens) / self.rate
time.sleep(wait) # ждём, а НЕ бьёмся в 429
# для LLM держим ДВА лимитера: по запросам (RPM) и по токенам (TPM)
rpm = TokenBucket(rate=50/60, capacity=50) # ~50 запросов/мин
tpm = TokenBucket(rate=100_000/60, capacity=100_000) # ~100k токенов/мин
def call_llm(prompt: str):
rpm.acquire(1)
tpm.acquire(estimate_tokens(prompt)) # оцениваем токены ЗАРАНЕЕ
return provider.generate(prompt)
Целевой темп R должен быть с запасом меньше лимита провайдера (например, 80–90%). Причины: оценка токенов неточна, есть параллельные инстансы твоего сервиса, делящие один общий лимит, и всплески. Упереться в свой лимит с очередью — контролируемо; упереться в лимит провайдера с 429 — нет. Клиентский limiter + backoff на 429 как страховка — рабочая связка.
Очереди и приоритеты
Раз запросы при нехватке токенов ждут, значит образуется очередь. И тут появляется вопрос: в каком порядке её обслуживать? По умолчанию FIFO (первым пришёл — первым ушёл), но у продукта часто есть приоритеты:
- Интерактивный vs фоновый. Запрос живого пользователя (ждёт ответа на экране) важнее фоновой batch-задачи (может подождать). Приоритетная очередь пропускает интерактивные вперёд.
- Платные vs бесплатные. Premium-пользователям — выше приоритет/своя квота.
- Дедлайны. Запросы с истекающим таймаутом (следующий урок) — вперёд, иначе они всё равно протухнут.
Если запросы приходят стабильно быстрее, чем limiter их выпускает, очередь растёт неограниченно — память течёт, а запросы в конце ждут так долго, что пользователь уже ушёл. Ставь предел длины очереди и предел времени ожидания: когда очередь переполнена — сразу отклоняй новые (fail fast / backpressure), а не принимай в бесконечное ожидание. Лучше честно сказать «сейчас перегружены, попробуйте позже», чем держать пользователя 2 минуты и всё равно затаймаутить.
Управление квотами между пользователями
Твой общий лимит от провайдера — один, а пользователей — много. Без распределения возникает проблема «шумного соседа»: один пользователь с тяжёлыми запросами (или циклящийся агент) выжирает весь общий лимит, и все остальные получают отказы из-за него. Нужно справедливое распределение квот.
| Подход | Как работает | Когда |
|---|---|---|
| Per-user лимит | Своя корзина/квота на каждого пользователя | Изоляция: один не мешает другим |
| Tier'ы | Разные квоты для free/pro/enterprise | Монетизация, приоритет платных |
| Fair queuing | Round-robin между пользователями в очереди | Справедливость при общем лимите |
| Дневные/месячные квоты | Бюджет токенов на период | Контроль расходов на пользователя |
Two-in-one: per-user лимиты защищают пользователей друг от друга (никто не съест общий пул), а глобальный лимит + бюджеты защищают тебя от разорения — сбойный или зловредный клиент, гоняющий агента в цикле, упрётся в свою квоту, а не сожжёт месячный бюджет за ночь (вспомни cost-алерты из урока про дашборды). Rate limiting — это не только про 429, но и про контроль расходов и абьюза.
Клиентский limiter выравнивает темп под текущий лимит, но если ты стабильно упираешься в потолок — это не проблема кода, а сигнал, что пора поднять лимит у провайдера (перейти на более высокий usage tier, запросить увеличение квоты). Лимиты у большинства провайдеров растут с историей потребления/оплаты. Rate limiting сглаживает пики; масштаб решается повышением самого лимита.
Типичные ошибки
Повторы бьются в тот же лимит, очередь растёт, латентность взрывается. Ставь клиентский rate limiter, чтобы 429 не возникал; backoff на 429 — лишь страховка.
У агентов TPM часто упирается первым (тяжёлые контексты). Лимитируй и запросы, и токены (оценивай токены заранее).
При устойчивом переполнении очередь растёт бесконечно — течёт память, запросы протухают. Ограничь длину и время ожидания, отклоняй лишнее сразу (fail fast).
«Шумный сосед» съедает весь пул, страдают все. Раздавай квоты по пользователям/tier'ам и защищайся от абьюза бюджетами.
Шпаргалка
429 = «ТЫ едешь слишком быстро» (не «сервер моргнул»)
ретраить 429 в лоб = бьёшься в ту же стену, очередь растёт → ПРЕДОТВРАЩАТЬ, не реагировать
ЛИМИТЫ ПРОВАЙДЕРА (двумерные у LLM):
RPM (запросы/мин) · TPM (токены/мин — часто упирается ПЕРВЫМ) · параллельные · дневные
агент = МНОГО вызовов на 1 запрос юзера → лимиты болезненны
КЛИЕНТСКИЙ RATE LIMITER (ограничитель на СВОЕЙ стороне):
token bucket: корзина пополняется R токенов/сек, запрос берёт токен;
нет токена → ЖДЁТ (а не 429) · ёмкость = burst
для LLM: ДВЕ корзины — RPM и TPM (оценивай токены ЗАРАНЕЕ)
✓ ставь R чуть НИЖЕ лимита провайдера (80–90%: неточность оценки, параллельные инстансы)
limiter + backoff на 429 (страховка) = рабочая связка
ОЧЕРЕДИ + ПРИОРИТЕТЫ:
интерактивный > фоновый · платный > free · горящий дедлайн — вперёд
⚠️ очередь НЕ бесконечна: лимит длины + времени ожидания → fail fast (backpressure)
КВОТЫ МЕЖДУ ЮЗЕРАМИ (общий лимит один, юзеров много):
per-user лимит (изоляция от «шумного соседа») · tier'ы (free/pro) ·
fair queuing · дневные/месячные бюджеты
🔒 защита юзеров друг от друга + защита ТЕБЯ от абьюза/разорения (cost-контроль)
растёшь → повышай usage tier у провайдера
ГРАБЛИ: ретрай 429 без лимитера · забыть TPM · безлимитная очередь · один пул без per-user
Практическое задание
Задание: приручи лимиты для агента
- Узнай RPM и TPM своего LLM-провайдера/tier'а. Прикинь: сколько вызовов LLM делает агент из модулей 03–05 на один запрос пользователя? Сколько параллельных пользователей упрут TPM?
- Объясни, почему ретрай 429 «в лоб» ухудшает ситуацию. Чем клиентский limiter принципиально лучше?
- Реализуй token bucket для RPM и для TPM (оценивай токены заранее). Поставь темп на 85% лимита провайдера. Проверь, что при всплеске запросы ждут, а не падают с 429.
- Добавь приоритетную очередь: интерактивные запросы — вперёд фоновых. Что делать, когда очередь переполнена?
- Смоделируй «шумного соседа»: один пользователь шлёт в 100× больше. Как per-user квота защитит остальных?
- Свяжи с бюджетом: как дневная квота токенов на пользователя защитит тебя от циклящегося агента (вспомни cost-алерты)?
- Со звёздочкой: если несколько инстансов твоего сервиса делят один лимит провайдера — как сделать распределённый rate limiter (подсказка: общий счётчик в Redis)? Почему локальный на каждом инстансе не спасёт?
Что дальше
Мы научились не превышать темп. Но даже проходящий запрос может зависнуть: агент в цикле, инструмент не отвечает, генерация тянется минутами. Держать пользователя (и ресурсы) в бесконечном ожидании нельзя. Следующий урок — timeout management: как ставить и каскадировать таймауты для долгих задач, чтобы агент не завис навсегда.