Проблема: провайдер считает твои запросы

Любой LLM-провайдер ставит лимиты на то, как быстро ты можешь к нему обращаться, — чтобы защитить свою инфраструктуру и справедливо поделить мощности между клиентами. Превысил — получаешь 429 Too Many Requests. У LLM лимиты обычно двумерные:

  • RPM (requests per minute) — сколько запросов в минуту.
  • TPM (tokens per minute) — сколько токенов в минуту (вход + выход). Часто именно TPM упирается первым, ведь один запрос агента может весить десятки тысяч токенов.
  • Иногда — лимиты на параллельные запросы, дневные квоты, бюджет.

Специфика агентов делает лимиты особенно болезненными: агент на один запрос пользователя делает много вызовов LLM (reasoning-цикл, вызовы инструментов, суммаризация). Десять параллельных пользователей → десятки одновременных вызовов → мгновенно упёрлись в TPM. И тут — ключевая ошибка новичка:

❌ Ретраить 429 «в лоб» — тушить пожар бензином

Если на 429 ты просто повторяешь запрос (даже с backoff), ты снова бьёшься в тот же лимит — а рядом копятся ещё запросы, тоже получающие 429 и тоже ретраящие. Очередь растёт, латентность взлетает, часть запросов вообще не проходит. 429 — это не «сервер моргнул», а «ты едешь слишком быстро». Лечение не в повторах, а в том, чтобы изначально не превышать темп. Backoff при 429 нужен (уважай Retry-After), но это подстраховка, а не решение.

Клиентский rate limiter

Идея: поставить ограничитель на своей стороне, который пропускает запросы к провайдеру не быстрее заданного темпа (чуть ниже лимита провайдера). Тогда 429 просто не возникает — запросы ждут своей очереди у тебя, а не отскакивают от провайдера.

Классический алгоритм — token bucket («корзина токенов»). Интуиция простая:

100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
Token bucket: корзина пополняется с фиксированной скоростью пополнение R токенов/сек корзина ёмкость = burst берёт токен есть токен? да → пропускаем → запрос к LLM ниже лимита, без 429 нет → ЖДЁТ в очереди пока корзина пополнится Средний темп = R (скорость пополнения) · всплеск до ёмкости (burst) ставь R чуть НИЖЕ лимита провайдера → 429 не возникает вовсе для LLM — обычно ДВЕ корзины: одна на RPM (запросы), одна на TPM (токены)

Корзина пополняется токенами с постоянной скоростью R (это твой целевой темп). Каждый запрос «забирает» токен: есть токен — проходит сразу; нет — ждёт, пока корзина пополнится. Ёмкость корзины задаёт допустимый всплеск (burst) — можно накопить и пропустить пачку разом. Средний темп при этом никогда не превышает R.

Клиентский rate limiter (token bucket, концептуально)
python
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 queuingRound-robin между пользователями в очередиСправедливость при общем лимите
Дневные/месячные квотыБюджет токенов на периодКонтроль расходов на пользователя
🔒 Защита от «шумного соседа» и защита от себя

Two-in-one: per-user лимиты защищают пользователей друг от друга (никто не съест общий пул), а глобальный лимит + бюджеты защищают тебя от разорения — сбойный или зловредный клиент, гоняющий агента в цикле, упрётся в свою квоту, а не сожжёт месячный бюджет за ночь (вспомни cost-алерты из урока про дашборды). Rate limiting — это не только про 429, но и про контроль расходов и абьюза.

✅ Растёшь — повышай tier у провайдера

Клиентский limiter выравнивает темп под текущий лимит, но если ты стабильно упираешься в потолок — это не проблема кода, а сигнал, что пора поднять лимит у провайдера (перейти на более высокий usage tier, запросить увеличение квоты). Лимиты у большинства провайдеров растут с историей потребления/оплаты. Rate limiting сглаживает пики; масштаб решается повышением самого лимита.

Типичные ошибки

❌ Ошибка 1: ретраить 429 без клиентского лимита

Повторы бьются в тот же лимит, очередь растёт, латентность взрывается. Ставь клиентский rate limiter, чтобы 429 не возникал; backoff на 429 — лишь страховка.

❌ Ошибка 2: учитывать только RPM, забыв TPM

У агентов TPM часто упирается первым (тяжёлые контексты). Лимитируй и запросы, и токены (оценивай токены заранее).

❌ Ошибка 3: неограниченная очередь

При устойчивом переполнении очередь растёт бесконечно — течёт память, запросы протухают. Ограничь длину и время ожидания, отклоняй лишнее сразу (fail fast).

❌ Ошибка 4: один общий лимит без per-user квот

«Шумный сосед» съедает весь пул, страдают все. Раздавай квоты по пользователям/tier'ам и защищайся от абьюза бюджетами.

Шпаргалка

Rate limiting и квоты — всё в одном месте
text
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

Практическое задание

Задание: приручи лимиты для агента

  1. Узнай RPM и TPM своего LLM-провайдера/tier'а. Прикинь: сколько вызовов LLM делает агент из модулей 03–05 на один запрос пользователя? Сколько параллельных пользователей упрут TPM?
  2. Объясни, почему ретрай 429 «в лоб» ухудшает ситуацию. Чем клиентский limiter принципиально лучше?
  3. Реализуй token bucket для RPM и для TPM (оценивай токены заранее). Поставь темп на 85% лимита провайдера. Проверь, что при всплеске запросы ждут, а не падают с 429.
  4. Добавь приоритетную очередь: интерактивные запросы — вперёд фоновых. Что делать, когда очередь переполнена?
  5. Смоделируй «шумного соседа»: один пользователь шлёт в 100× больше. Как per-user квота защитит остальных?
  6. Свяжи с бюджетом: как дневная квота токенов на пользователя защитит тебя от циклящегося агента (вспомни cost-алерты)?
  7. Со звёздочкой: если несколько инстансов твоего сервиса делят один лимит провайдера — как сделать распределённый rate limiter (подсказка: общий счётчик в Redis)? Почему локальный на каждом инстансе не спасёт?

Что дальше

Мы научились не превышать темп. Но даже проходящий запрос может зависнуть: агент в цикле, инструмент не отвечает, генерация тянется минутами. Держать пользователя (и ресурсы) в бесконечном ожидании нельзя. Следующий урок — timeout management: как ставить и каскадировать таймауты для долгих задач, чтобы агент не завис навсегда.