Rate Limiting для API веб-застосунку
Rate limiting обмежує кількість запитів від одного джерела за одиницю часу. Захищає від brute force, credential stuffing, DDoS на рівні застосунку та неконтрольованого споживання ресурсів партнерськими інтеграціями. Без rate limiting один клієнт з багом (нескінченний retry-loop) може збити все застосування.
Алгоритми
Fixed Window — лічильник скидається кожні N секунд. Простий у реалізації, але уразливий до burst в момент скидання: 100 запитів в кінці вікна + 100 на початку наступного = 200 за секунду.
Sliding Window — усереднення за ковзним вікном. Рівніше розподіляє навантаження.
Token Bucket — накопичує «токени» зі швидкістю refill rate, кожен запит витрачає один токен. Дозволяє burst до bucket size, потім обмеження.
Leaky Bucket — черга запитів з фіксованим drain rate. Максимально рівне навантаження.
Для більшості веб-застосунків достатньо Sliding Window з Redis.
Реалізація в Laravel
Laravel Throttle middleware з коробки використовує cache (Redis):
// routes/api.php
Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
Route::apiResource('articles', ArticleController::class);
});
// config/cache.php — ліміти через RateLimiter
// app/Providers/RouteServiceProvider.php
RateLimiter::for('api', function (Request $request) {
return $request->user()
? Limit::perMinute(300)->by($request->user()->id)
: Limit::perMinute(60)->by($request->ip());
});
// Різні ліміти для різних тарифів
RateLimiter::for('api', function (Request $request) {
$user = $request->user();
if (!$user) return Limit::perMinute(30)->by($request->ip());
return match($user->plan) {
'enterprise' => Limit::perMinute(1000)->by($user->id),
'pro' => Limit::perMinute(300)->by($user->id),
default => Limit::perMinute(60)->by($user->id),
};
});
Laravel автоматично додає заголовки X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After.
Реалізація в NestJS + Redis
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis';
// app.module.ts
ThrottlerModule.forRoot({
throttlers: [
{ name: 'short', ttl: 1000, limit: 10 }, // 10 req/sec
{ name: 'medium', ttl: 60000, limit: 300 }, // 300 req/min
{ name: 'long', ttl: 3600000, limit: 5000 }, // 5000 req/hour
],
storage: new ThrottlerStorageRedisService(redisClient),
}),
// Декоратор на конкретний ендпоінт
@Throttle({ short: { limit: 3, ttl: 60000 } }) // 3 запитів на хвилину
@Post('auth/login')
async login(@Body() dto: LoginDto) { ... }
Rate limiting на рівні Nginx
Перша лінія захисту до PHP/Node:
# limit_req_zone — визначаємо зони
limit_req_zone $binary_remote_addr zone=api_general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=api_auth:10m rate=3r/m;
server {
location /api/ {
limit_req zone=api_general burst=20 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
location /api/auth/ {
limit_req zone=api_auth burst=5 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
}
burst=20 nodelay — дозволяє всплеск до 20 запитів одночасно без затримки, потім жорстке обмеження.
Заголовки відповіді
Правильні заголовки rate limit — частина контракту API:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1735689600
Retry-After: 47
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Перевищено ліміт запитів. Повторіть через 47 секунд.",
"retry_after": 47
}
}
Retry-After — Unix timestamp або секунди. Клієнт має поважати його й не намагатися раніше.
Distributed rate limiting
При кількох серверах застосунку — лічильники потрібно зберігати централізовано. Redis + Lua-скрипт для атомарного Sliding Window:
-- sliding_window.lua
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now .. math.random())
redis.call('EXPIRE', key, window / 1000)
return 1
end
return 0
Bypass-стратегії
Не всі запити мають витрачати ліміт:
- Внутрішні сервіси (IP-whitelist або service token з безлімітним rate)
- Webhook endpoint (приймає вхідні від зовнішніх сервісів — обмежувати не можна)
- Health check
/health— не обмежувати
RateLimiter::for('api', function (Request $request) {
if ($request->ip() === config('services.internal_ip')) {
return Limit::none();
}
// ...
});
Строки
Rate limiting з Redis (Sliding Window, різні ліміти по тарифах, правильні заголовки): 1–2 дня. З Nginx-рівнем, Lua-скриптом для distributed counting, моніторингом 429-відповідей у Grafana: 3–4 дня.







