Rate limiting для AI-агентів: як стримати сплески запитів і retry-шторм

Практичний rate limiting у production: per-user/per-tenant/global ліміти, burst-контроль, retry_after, backoff, audit logs і alerting.
На цій сторінці
  1. Ідея за 30 секунд
  2. Проблема
  3. Рішення
  4. Rate limiting ≠ step limits
  5. Компоненти rate-limiting контролю
  6. Як це виглядає в архітектурі
  7. Приклад
  8. У коді це виглядає так
  9. Як це виглядає під час виконання
  10. Сценарій 1: зупинка по tenant-ліміту
  11. Сценарій 2: burst-сплеск
  12. Сценарій 3: нормальне виконання
  13. Типові помилки
  14. Самоперевірка
  15. FAQ
  16. Де Rate Limiting у загальній системі
  17. Пов'язані сторінки

Ідея за 30 секунд

Rate limiting — це runtime-контроль, який обмежує частоту зовнішніх викликів агента, щоб не допустити сплесків і retry-шторму в production.

Коли це потрібно: коли агент часто викликає model/tool API, має retry-логіку і працює під піковим навантаженням.

Проблема

Без rate limiting один нестабільний сервіс швидко розганяє ланцюг: retry → виклик → ще retry. У demo це майже не видно. У production така поведінка створює лавину 429/5xx, черги і затримки.

Найгірше, що інцидент масштабується сам:

  • один надто активний користувач забирає квоти
  • один tenant "душить" інших
  • глобальний пік ламає залежності одразу для всіх

І кожна хвилина без обмеження частоти додає нові ретраї, черги і затримки, і система сама себе DDoS-ить.

Аналогія: це як рампа на в'їзді на автомагістраль. Якщо не дозувати потік, навіть добра дорога стає затором за хвилини.

Рішення

Рішення — додати централізований rate-limit policy layer у runtime і tool gateway. Кожен зовнішній виклик агента перевіряється за лімітами per_user, per_tenant, global і burst_tokens.

Policy повертає технічне рішення: allow або stop з явною причиною:

  • rate_limited_user
  • rate_limited_tenant
  • rate_limited_global
  • burst_limited

Коли повертається stop, runtime повертає retry_after_ms клієнту і не виконує виклик. Це окремий рівень системи, а не частина промпта чи логіки моделі.

Rate limiting ≠ step limits

Це різні рівні контролю:

  • Rate limiting обмежує частоту зовнішніх викликів.
  • Step limits обмежують довжину і поведінку runtime loop.

Одне без іншого не працює:

  • без rate limiting зовнішні API падають від піків і retry-шторму
  • без step limits run може довго крутитися навіть із помірною частотою викликів

Приклад:

  • rate limiting: не більше per_user=6 викликів за 10 секунд
  • step limits: max_steps=18, max_repeat_action=3

Компоненти rate-limiting контролю

Ці компоненти працюють разом на кожному зовнішньому виклику агента.

КомпонентЩо контролюєКлючові механікиНавіщо
Per-user limitПоведінку одного користувачаper_user quota
sliding window
Не дає одному користувачу "з'їсти" весь ресурс
Per-tenant limitНавантаження одного tenantper_tenant quota
tenant-scoped keys
Ізолює піки між клієнтами
Global limitЗагальне навантаження системиglobal cap
shared limiter
Захищає зовнішні залежності від масового сплеску
Burst controlКороткі пікові "спайки"token bucket
refill rate
Гасить миттєві стрибки без повного стопу системи
Rate-limit observabilityВидимість policy-рішеньaudit logs
alerts на stop spikes
Не обмежує виклики напряму, але дозволяє швидко знайти джерело піку

Приклад alert:

Slack: 🛑 Support-Agent hit rate_limited_tenant. retry_after=1200ms, tenant=t_42.

Як це виглядає в архітектурі

Rate-limit policy layer стоїть між runtime і зовнішніми model/tool API. Кожне рішення (allow або stop) фіксується в audit log.

Кожен зовнішній виклик агента проходить через цей flow перед виконанням: runtime не виконує виклик напряму, а передає рішення policy layer.

Коротко по flow:

  • Runtime формує зовнішній виклик агента
  • Policy перевіряє per_user, per_tenant, global і burst_tokens
  • allow -> виклик виконується
  • stop -> повертається retry_after_ms і часткова відповідь
  • обидва рішення пишуться в audit log

Приклад

Агент підтримки одночасно обробляє багато запитів і кілька разів ретраїть crm.search.

З rate limiting:

  • per_user = 6 / 10s
  • per_tenant = 120 / min
  • global = 50 / s
  • burst_tokens = 5

-> сплеск зупиняється на policy-рівні до падіння залежностей і черг.

Rate limiting зупиняє інцидент прямо перед зовнішнім викликом, а не після хвилі 429.

У коді це виглядає так

У спрощеній схемі вище показано основний flow. Критично: rate-limit check має бути O(1) і атомарним (зазвичай через Redis/Lua або еквівалент), інакше під піком він сам стає вузьким місцем. Після stop(...) runtime зазвичай формує часткову відповідь для клієнта з явною причиною і retry_after_ms.

Приклад rate-limit конфігурації:

YAML
rate_limits:
  per_user_10s: 6
  per_tenant_min: 120
  global_rps: 50
  burst_tokens: 5
  refill_per_second: 2
PYTHON
action = planner.next(state)
action_key = make_action_key(action.name, action.args)

if not action.is_external_call():
    # execute_local — умовний helper для локальних дій без зовнішнього API
    # Decision.allow — умовний helper, щоб зберегти єдину модель outcome/reason.
    local_result = execute_local(action)
    local_decision = Decision.allow(reason=None)
    audit.log(
        run_id,
        decision=local_decision.outcome,
        reason=local_decision.reason,
        scope="local",
        action=action.name,
        action_key=action_key,
        result=local_result.status,
    )
    return local_result

decision = rate_limit.check(
    user_id=state.user_id,
    tenant_id=state.tenant_id,
    action=action.name,
    now_ms=clock.now_ms(),
)

if decision.outcome == "stop":
    audit.log(
        run_id,
        decision=decision.outcome,
        reason=decision.reason,
        scope=decision.scope,
        retry_after_ms=decision.retry_after_ms,
        action=action.name,
        action_key=action_key,
    )
    alerts.notify_if_needed(run_id, decision.reason, scope=decision.scope)
    return stop(decision.reason, retry_after_ms=decision.retry_after_ms)

result = executor.execute(action)

audit.log(
    run_id,
    decision=decision.outcome,
    reason=decision.reason,
    scope=decision.scope,
    action=action.name,
    action_key=action_key,
    result=result.status,
)

return result

Як це виглядає під час виконання

Сценарій 1: зупинка по tenant-ліміту

  1. Runtime формує зовнішній виклик crm.search.
  2. Policy бачить перевищення per_tenant квоти.
  3. Рішення: stop (reason=rate_limited_tenant).
  4. Runtime повертає retry_after_ms.
  5. Виклик не виконується, подія пишеться в audit log.

Сценарій 2: burst-сплеск

  1. Кілька run-ів одночасно роблять короткий сплеск викликів.
  2. Policy вичерпує burst_tokens.
  3. Рішення: stop (reason=burst_limited).
  4. Частина викликів відхиляється з retry_after_ms.
  5. Система лишається стабільною без cascade-failure.

Сценарій 3: нормальне виконання

  1. Runtime формує зовнішній виклик.
  2. Policy перевіряє ліміти: все в межах.
  3. Рішення: allow.
  4. Виклик виконується.
  5. Рішення і результат пишуться в audit log.

Типові помилки

  • ставити тільки global limit без per_user/per_tenant ізоляції
  • не віддавати retry_after при stop
  • робити retry без backoff і jitter
  • перевіряти rate limit лише в одному шарі (тільки runtime або тільки gateway)
  • не логувати stop-рішення (reason, scope, retry_after_ms)
  • не ставити alerting на сплеск rate_limited_*

У результаті система виглядає контрольованою, але при реальному піку швидко деградує.

Самоперевірка

Швидка перевірка rate limiting перед запуском у production:

Прогрес: 0/8

⚠ Бракує базового governance-контролю

Перед production потрібні мінімум: контроль доступу, ліміти, audit logs і аварійна зупинка.

FAQ

Q: З яких лімітів почати?
A: Мінімум: per_user, per_tenant, global + невеликий burst-контроль. Далі підлаштовуй за реальними stop-подіями.

Q: Якщо зовнішній API вже має 429, чи потрібен наш rate limiting?
A: Так. Власний rate limiting захищає runtime до зовнішнього 429 і дає контрольовані stop reasons, retry_after та аудит.

Q: Що краще при ліміті: стоп чи черга?
A: Для sync-run зазвичай краще stop + retry_after. Для async-пайплайнів можна додавати чергу, але все одно з явними лімітами і timeout.

Q: Rate limiting замінює budget controls?
A: Ні. Rate limiting контролює частоту викликів, а budget controls — сумарні витрати run.

Q: Де зберігати лічильники?
A: У спільному low-latency сховищі з атомарними операціями (часто Redis). Без цього ліміти між інстансами будуть неконсистентними.

Де Rate Limiting у загальній системі

Rate limiting — це один із рівнів Agent Governance. Разом із RBAC, бюджетами, step limits, approval і audit він формує єдину систему контролю виконання.

Пов'язані сторінки

Далі за темою:

⏱️ 6 хв читанняОновлено 27 березня 2026 р.Складність: ★★★
Реалізувати в OnceOnly
Budgets + permissions you can enforce at the boundary.
Використати в OnceOnly
# onceonly guardrails (concept)
version: 1
budgets:
  max_steps: 25
  max_tool_calls: 12
  max_seconds: 60
  max_usd: 1.00
policy:
  tool_allowlist:
    - search.read
    - http.get
writes:
  require_approval: true
  idempotency: true
controls:
  kill_switch: { enabled: true }
Інтегровано: продакшен-контрольOnceOnly
Додай guardrails до агентів з tool-calling
Зашип цей патерн з governance:
  • Бюджетами (кроки / ліміти витрат)
  • Дозволами на інструменти (allowlist / blocklist)
  • Kill switch та аварійна зупинка
  • Ідемпотентність і dedupe
  • Audit logs та трасування
Інтегрована згадка: OnceOnly — контрольний шар для продакшен агент-систем.

Автор

Микола — інженер, який будує інфраструктуру для продакшн AI-агентів.

Фокус: патерни агентів, режими відмов, контроль рантайму та надійність систем.

🔗 GitHub: https://github.com/mykolademyanov


Редакційна примітка

Ця документація підготовлена з допомогою AI, із людською редакторською відповідальністю за точність, ясність і продакшн-релевантність.

Контент базується на реальних відмовах, постмортемах та операційних інцидентах у розгорнутих AI-агентних системах.