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 дня.







