Throttling API для веб-застосунку
Throttling — управління швидкістю обробки запитів на рівні сервера, на відміну від rate limiting, який обмежує клієнта. Throttling сповільнює або ставить в чергу вхідні запитів, щоб захистити бекенд від перегрузу. Різниця принципова: rate limit говорить «ти зробив занадто багато запитів», throttling говорить «ми обробляємо стільки, скільки можемо».
Throttling vs Rate Limiting
| Аспект | Rate Limiting | Throttling |
|---|---|---|
| Суб'єкт | Клієнт (IP, user_id) | Сервер (CPU, черга) |
| Дія при перевищенні | 429, запит відхилено | Запит затримано або поставлено в чергу |
| Мета | Захист від зловживань | Захист ресурсів бекенду |
| Відповідь клієнту | Негайна 429 | Затримка або 503 |
На практиці обидва механізми застосовуються разом.
Throttling важких операцій
Деякі операції — експорт звіту, обробка файлу, розсилання email — не мають виконуватися паралельно в необмеженій кількості:
// BullMQ — throttle через concurrency + rateLimit
const queue = new Queue('reports', { connection: redis });
const worker = new Worker('reports', processReport, {
connection: redis,
concurrency: 5, // максимум 5 паралельних задач
limiter: {
max: 10, // 10 задач
duration: 60_000, // за 60 секунд
},
});
// Додавання задачі з пріоритетом
await queue.add('generate-csv', { userId, filters }, {
priority: user.plan === 'enterprise' ? 1 : 10,
attempts: 3,
backoff: { type: 'exponential', delay: 2000 },
});
Adaptive throttling
Адаптивний throttling знижує ліміти при зростанні latency або помилок:
class AdaptiveThrottler {
private limit = 100;
private readonly minLimit = 10;
private readonly maxLimit = 100;
async check(): Promise<boolean> {
const metrics = await this.getMetrics();
// Знижуємо ліміт при високому p95 latency
if (metrics.p95Latency > 500) {
this.limit = Math.max(this.minLimit, this.limit * 0.8);
} else if (metrics.p95Latency < 200 && metrics.errorRate < 0.01) {
this.limit = Math.min(this.maxLimit, this.limit * 1.1);
}
return this.counter.increment() <= this.limit;
}
}
Google використовує аналогічний механізм у своїх сервісах («Client-Side Throttling» з SRE book).
Circuit Breaker
Throttling для вихідних запитів до зовнішніх API — Circuit Breaker паттерн:
import CircuitBreaker from 'opossum';
const options = {
timeout: 3000, // запит > 3 секунди = fail
errorThresholdPercentage: 50, // 50% помилок → open
resetTimeout: 30000, // через 30 сек пробуємо снова (half-open)
volumeThreshold: 10, // мінімум 10 запитів для підрахунку
};
const breaker = new CircuitBreaker(callExternalAPI, options);
breaker.on('open', () => logger.warn('Circuit breaker OPEN — external API unavailable'));
breaker.on('halfOpen', () => logger.info('Circuit breaker HALF-OPEN — testing'));
breaker.on('close', () => logger.info('Circuit breaker CLOSE — external API recovered'));
// Fallback при відкритому circuit
breaker.fallback(() => ({ status: 'cached', data: getCachedData() }));
Стани: Closed (норма) → Open (занадто багато помилок, запити не відправляються) → Half-Open (пробний запит) → Closed (якщо успішно).
Throttling вхідних webhook'ів
Партнери можуть надсилати тисячи webhook'ів одночасно (наприклад, при масовому оновленні статусів замовлень). Правильний паттерн — прийняти швидко (202), поставити в чергу:
// WebhookController.php — негайна відповідь
public function handle(Request $request)
{
$payload = $request->all();
$signature = $request->header('X-Signature');
if (!$this->verifySignature($payload, $signature)) {
return response()->json(['error' => 'Invalid signature'], 401);
}
// Кладемо в чергу з throttle
ProcessWebhook::dispatch($payload)
->onQueue('webhooks')
->delay(now()); // негайно, але через чергу
return response()->json(['accepted' => true], 202);
}
// config/queue.php — ліміт воркерів для черги webhooks
// Horizon:
'webhooks' => [
'connection' => 'redis',
'queue' => ['webhooks'],
'balance' => 'auto',
'maxProcesses' => 10, // не більше 10 паралельних
],
Throttling в Nginx upstream
upstream backend {
server app1:3000;
server app2:3000;
# Обмеження одночасних з'єднань до upstream
keepalive 32;
}
# queue — буферизація при перегрузі
location /api/ {
proxy_pass http://backend;
proxy_connect_timeout 1s;
proxy_read_timeout 30s;
# Якщо backend не відповідає швидко — 503 замість зависання
proxy_next_upstream error timeout http_503;
proxy_next_upstream_tries 2;
}
Моніторинг throttling
Метрики для дашборду:
// Prometheus counters
throttleRejected.inc({ reason: 'queue_full', endpoint: '/api/export' });
throttleDelayed.observe({ endpoint: '/api/export' }, delayMs);
queueDepth.set({ queue: 'reports' }, await queue.count());
Алерт: queue depth > 1000 протягом 5 хвилин → Scale up workers або сповіщення дежурному.
Строки
BullMQ з concurrency + rateLimit, circuit breaker для зовнішніх API, webhook-черга: 3–5 днів. З adaptive throttling, Prometheus-метриками, Grafana-дашбордом і алертами: 1–2 тижні.







