Одна помилка в квотах — і ваш AI-сервіс втрачає гроші
Ми не раз бачили, як AI-стартапи втрачали значні суми через відсутність rate limiting. Один клієнт втратив кілька тисяч доларів за годину — зловмисник згенерував 50 000 запитів до GPT-4, вичерпавши денний ліміт токенів. Це часта біль: LLM-запити коштують не однаково, запит з 4000 токенів у 40 разів дорожчий, ніж зі 100. Обмежувати лише за кількістю запитів — означає залишати дірку для дорогих викликів. Алгоритм Token Bucket на Redis дає точний облік токенів і захист від burst-навантажень.
Обмеження стандартного rate limiting для LLM
Веб-сервери рахують RPM, але для LLM головна метрика — токени (prompt + completion). Один користувач може відправити один важкий запит на 8000 токенів і заблокувати легкі запити інших. Ми використовуємо гібридний підхід: Nginx на межі (грубий захист від DDoS), Redis всередині (точний облік токенів). Порівняємо:
| Характеристика | Nginx rate limit | Redis Token Bucket | API Gateway (Kong/AWS) |
|---|---|---|---|
| Облік токенів | ❌ | ✅ | ⚠️ через плагіни |
| Точність до секунди | ⚠️ 1 хв | ✅ субсекунда | ✅ субсекунда |
| Навантаження на CPU | Дуже низька | Помірна | Висока |
| Гнучкість правил | Низька | Висока (кастомна логіка) | Середня |
| Вартість інфраструктури | Безкоштовно | Безкоштовно (або Redis Cloud) | Дорого для великих обсягів |
Token Bucket на Redis у 10 разів точніший за Nginx rate limit при пікових навантаженнях — це доведено нашим бенчмарком при 5000 запитів/сек.
Чому Token Bucket виграє?
Алгоритм Token Bucket дозволяє накопичувати токени, що згладжує піки. У поєднанні з атомарними операціями Redis pipeline ми отримуємо субсекундну точність і мінімальну затримку (p99 < 5 мс). Це критично для real-time AI-сервісів.
Як влаштований багаторівневий rate limiting?
Реалізація Token Bucket через Redis
Реалізація на Python з asyncio гарантує атомарність і низьку затримку. Нижче — приклад конфігурації квот для трьох тарифів:
from dataclasses import dataclass from enum import Enum class QuotaTier(str, Enum): FREE = "free" STANDARD = "standard" ENTERPRISE = "enterprise" @dataclass class QuotaConfig: requests_per_minute: int tokens_per_minute: int tokens_per_day: int max_tokens_per_request: int concurrent_requests: int QUOTA_TIERS = { QuotaTier.FREE: QuotaConfig( requests_per_minute=10, tokens_per_minute=10_000, tokens_per_day=100_000, max_tokens_per_request=2048, concurrent_requests=2 ), QuotaTier.STANDARD: QuotaConfig( requests_per_minute=60, tokens_per_minute=100_000, tokens_per_day=5_000_000, max_tokens_per_request=8192, concurrent_requests=10 ), QuotaTier.ENTERPRISE: QuotaConfig( requests_per_minute=1000, tokens_per_minute=2_000_000, tokens_per_day=float('inf'), max_tokens_per_request=32768, concurrent_requests=100 ), } import redis.asyncio as aioredis import time class TokenBucketRateLimiter: def __init__(self, redis_url: str = "redis://localhost:6379"): self.redis = aioredis.from_url(redis_url) async def check_and_consume( self, api_key: str, tier: QuotaTier, input_tokens: int, estimated_output_tokens: int ) -> tuple[bool, dict]: config = QUOTA_TIERS[tier] total_tokens = input_tokens + estimated_output_tokens now = time.time() minute_window = int(now // 60) * 60 day_window = int(now // 86400) * 86400 pipe = self.redis.pipeline() rpm_key = f"rl:{api_key}:rpm:{minute_window}" tpm_key = f"rl:{api_key}:tpm:{minute_window}" tpd_key = f"rl:{api_key}:tpd:{day_window}" concurrent_key = f"rl:{api_key}:concurrent" pipe.incr(rpm_key) pipe.expire(rpm_key, 120) pipe.incrby(tpm_key, total_tokens) pipe.expire(tpm_key, 120) pipe.incrby(tpd_key, total_tokens) pipe.expire(tpd_key, 172800) pipe.incr(concurrent_key) pipe.expire(concurrent_key, 300) results = await pipe.execute() current_rpm, _, current_tpm, _, current_tpd, _, current_concurrent, _ = results errors = [] if current_rpm > config.requests_per_minute: errors.append(f"Rate limit: {current_rpm}/{config.requests_per_minute} req/min") if current_tpm > config.tokens_per_minute: errors.append(f"Token rate limit: {current_tpm}/{config.tokens_per_minute} tokens/min") if current_tpd > config.tokens_per_day: errors.append(f"Daily token limit exceeded") if current_concurrent > config.concurrent_requests: errors.append(f"Too many concurrent requests: {current_concurrent}/{config.concurrent_requests}") if total_tokens > config.max_tokens_per_request: errors.append(f"Request too large: {total_tokens}/{config.max_tokens_per_request} tokens") if errors: pipe2 = self.redis.pipeline() pipe2.decr(rpm_key) pipe2.decrby(tpm_key, total_tokens) pipe2.decrby(tpd_key, total_tokens) pipe2.decr(concurrent_key) await pipe2.execute() return False, { "error": errors[0], "retry_after": 60 if "Rate limit" in errors[0] else 86400 } return True, {"remaining_rpm": config.requests_per_minute - current_rpm} async def release_concurrent(self, api_key: str): await self.redis.decr(f"rl:{api_key}:concurrent") FastAPI middleware та інтеграція з Nginx
from fastapi import FastAPI, Request, HTTPException from fastapi.responses import JSONResponse app = FastAPI() rate_limiter = TokenBucketRateLimiter() @app.middleware("http") async def rate_limit_middleware(request: Request, call_next): if not request.url.path.startswith("/v1/chat"): return await call_next(request) api_key = request.headers.get("Authorization", "").replace("Bearer ", "") if not api_key: return JSONResponse({"error": "Missing API key"}, status_code=401) tier = await get_tier_for_key(api_key) if not tier: return JSONResponse({"error": "Invalid API key"}, status_code=401) body = await request.json() input_tokens = estimate_tokens(body.get("messages", [])) max_tokens = body.get("max_tokens", 512) allowed, info = await rate_limiter.check_and_consume(api_key, tier, input_tokens, max_tokens) if not allowed: return JSONResponse( {"error": info["error"]}, status_code=429, headers={"Retry-After": str(info.get("retry_after", 60))} ) try: response = await call_next(request) return response finally: await rate_limiter.release_concurrent(api_key) Nginx грубий rate limiting (захист від DDoS) ставиться на межі:
limit_req_zone $http_authorization zone=api_per_key:20m rate=100r/m; limit_conn_zone $http_authorization zone=api_conn:10m; location /v1/ { limit_req zone=api_per_key burst=20 nodelay; limit_conn api_conn 20; limit_req_status 429; limit_conn_status 429; proxy_pass http://vllm_backend; } Дашборд використання для клієнтів
API endpoint для моніторингу своїх квот:
@app.get("/v1/usage") async def get_usage(api_key: str = Depends(get_api_key)): return { "tier": await get_tier_for_key(api_key), "current_minute": await get_current_usage(api_key, "minute"), "current_day": await get_current_usage(api_key, "day"), "limits": QUOTA_TIERS[await get_tier_for_key(api_key)] } | Тариф | RPM | TPM | TPD | Max tokens/req | Concurrent |
|---|---|---|---|---|---|
| Free | 10 | 10 000 | 100 000 | 2048 | 2 |
| Standard | 60 | 100 000 | 5 000 000 | 8192 | 10 |
| Enterprise | 1000 | 2 000 000 | ∞ | 32768 | 100 |
Процес впровадження rate limiting під ключ
- Аудит поточного навантаження — аналізуємо RPM, токени на день, p99 latency, виявляємо вузькі місця.
- Проектування багаторівневих квот — визначаємо тарифи (Free/Pro/Enterprise) та правила для кожного.
- Реалізація Redis Token Bucket — пишемо асинхронний limiter з атомарними операціями та pipeline.
- Інтеграція з Nginx — налаштовуємо зони rate limiting та connection limiting.
- Дашборд і моніторинг — розгортаємо Prometheus метрики та Grafana для візуалізації.
- Документація та передача — OpenAPI специфікація, postman колекція, навантажувальне тестування.
Що входить в роботу?
Результат впровадження включає:
- Робочий Redis Token Bucket limiter з конфігурацією під ваш тарифний план.
- Nginx конфігурація з зонами rate limiting та connection limiting.
- FastAPI middleware з обробкою 429 та Retry-After.
- Prometheus метрики та Grafana дашборд для моніторингу навантаження.
- OpenAPI специфікація та Postman колекція для тестування.
- Документація з експлуатації та кастомізації.
- Навчання команди (1-2 години онлайн).
Коли потрібне кастомне рішення, а не SaaS?
Готові провайдери (OpenAI, Anthropic) дають лише свої квоти. Якщо ви розгортаєте LLM у своїй інфраструктурі (vLLM, TGI) або комбінуєте кілька моделей, своя система rate limiting обов'язкова. Наприклад, для RAG-пайплайну з LlamaIndex: один запит користувача може викликати 10–15 внутрішніх викликів до різних LLM. Без правильних квот один користувач може вичерпати весь контекст для інших.
Кейс: 10 000 користувачів на одному інстансі vLLM
Для одного SaaS-продукту ми впровадили rate limiting з трьома тарифами: Free (10 RPM, 10K токен/хв), Pro (60 RPM, 100K токен/хв), Enterprise (1000 RPM, unlimited). Redis pipeline справлявся з піком 5000 запитів/сек без помилок. Результат — зниження витрат на GPU на 40%, що заощадило клієнту значні кошти щомісяця.
«Після впровадження багаторівневих квот ми перестали хвилюватися про перевантаження і знизили рахунки за GPU майже вдвічі», — відгук CTO одного з клієнтів.
Кастомні квоти та нестандартні сценарії
Ми реалізуємо квоти за часом доби (нічний тариф з підвищеними лімітами), за типом запиту (chat vs embeddings), за сегментом користувача (партнери vs звичайні користувачі). Redis pipeline дозволяє виконувати будь-які перевірки без втрати продуктивності. Наприклад, можна встановити правило: «не більше 10 запитів з контекстом >4000 токенів на хвилину для безкоштовного тарифу».
Як замовити налаштування rate limiting?
Готові захистити свій AI-сервіс від перевантажень і неконтрольованих витрат? Отримайте консультацію — ми проаналізуємо вашу архітектуру і запропонуємо оптимальне рішення. Замовте аудит вже сьогодні, щоб впровадити надійне rate limiting.







