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 недели.







