Админка
пожалуйста подождите

Ограничение частоты запросов защищает ваш API от злоупотреблений, обеспечивает справедливое распределение ресурсов и поддерживает доступность сервиса. Независимо от того, предотвращаете ли вы DDoS-атаки или управляете квотами API, корректное ограничение частоты запросов критически важно для production API. В этом руководстве рассматривается внедрение эффективного rate limiting с точки зрения senior-разработчика.

Почему ограничение частоты запросов важно

Ограничение частоты запросов защищает от:

  1. DDoS-атак: предотвращает перегрузку сервиса
  2. Brute force: блокирует попытки credential stuffing
  3. Злоупотребления ресурсами: справедливое использование среди клиентов
  4. Контроля затрат: ограничивает дорогостоящие операции
  5. Монетизации API: обеспечивает соблюдение лимитов тарифного плана

Стратегии ограничения частоты запросов

Fixed Window

Подсчитывайте запросы в фиксированных временных периодах (например, 100 запросов в минуту).

Плюсы: просто реализовать Минусы: всплески на границах окна

// Node.js с Redis
const Redis = require('ioredis');
const redis = new Redis();
async function fixedWindowLimit(userId, limit, windowSeconds) {
const key = `ratelimit:${userId}:${Math.floor(Date.now() / 1000 / windowSeconds)}`;
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, windowSeconds);
}
return {
allowed: current <= limit,
remaining: Math.max(0, limit - current),
resetAt: Math.ceil(Date.now() / 1000 / windowSeconds) * windowSeconds
};
}

Sliding Window Log

Отслеживайте временные метки каждого запроса, подсчитывайте в пределах скользящего окна.

Плюсы: более плавное ограничение Минусы: высокая нагрузка на память при большом трафике

async function slidingWindowLog(userId, limit, windowSeconds) {
const key = `ratelimit:${userId}`;
const now = Date.now();
const windowStart = now - (windowSeconds * 1000);
// Удалить старые записи и добавить новую
const multi = redis.multi();
multi.zremrangebyscore(key, 0, windowStart);
multi.zadd(key, now, `${now}-${Math.random()}`);
multi.zcard(key);
multi.expire(key, windowSeconds);
const results = await multi.exec();
const count = results[2][1];
return {
allowed: count <= limit,
remaining: Math.max(0, limit - count),
count
};
}

Token Bucket

Токены добавляются с фиксированной скоростью, запросы потребляют токены.

Плюсы: допускает контролируемые всплески Минусы: более сложная реализация

async function tokenBucket(userId, bucketSize, refillRate, refillInterval) {
const key = `tokenbucket:${userId}`;
const now = Date.now();
// Получить или инициализировать bucket
let bucket = await redis.hgetall(key);
if (!bucket.tokens) {
bucket = { tokens: bucketSize, lastRefill: now };
}
// Рассчитать количество токенов для добавления
const timePassed = now - parseInt(bucket.lastRefill);
const intervalsPasssed = Math.floor(timePassed / refillInterval);
const tokensToAdd = intervalsPasssed * refillRate;
const newTokens = Math.min(bucketSize, parseInt(bucket.tokens) + tokensToAdd);
// Попытаться потратить токен
if (newTokens >= 1) {
await redis.hset(key, {
tokens: newTokens - 1,
lastRefill: bucket.lastRefill + (intervalsPasssed * refillInterval)
});
await redis.expire(key, 3600);
return { allowed: true, remaining: newTokens - 1 };
}
return { allowed: false, remaining: 0 };
}

Middleware Express.js

Базовая реализация

const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');
const redis = new Redis();
// Базовый rate limiter
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 минута
max: 100, // 100 запросов в минуту
standardHeaders: true, // Возвращать информацию о лимите в заголовках
legacyHeaders: false,
message: {
error: 'Too many requests',
retryAfter: 60
}
});
app.use('/api', limiter);

На базе Redis для распределённых систем

const limiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
standardHeaders: true,
store: new RedisStore({
sendCommand: (...args) => redis.call(...args),
prefix: 'rl:'
}),
keyGenerator: (req) => {
// Использовать ID пользователя при аутентификации, иначе — IP
return req.user?.id || req.ip;
},
skip: (req) => {
// Пропускать ограничение частоты запросов при определённых условиях
return req.user?.role === 'admin';
},
handler: (req, res) => {
res.status(429).json({
error: 'Rate limit exceeded',
retryAfter: Math.ceil(req.rateLimit.resetTime / 1000)
});
}
});

Несколько rate limiter’ов

// Строгий limiter для auth endpoint’ов
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 минут
max: 5, // 5 попыток за 15 минут
message: { error: 'Too many login attempts' }
});
// Стандартный limiter для API
const apiLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100
});
// Более щедрый limiter для endpoint’ов только для чтения
const readLimiter = rateLimit({
windowMs: 60 * 1000,
max: 1000
});
// Применить к маршрутам
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);
app.get('/api/public/*', readLimiter);
app.use('/api', apiLimiter);

Ограничение частоты запросов в Laravel

Ограничение на уровне маршрутов

// routes/api.php
Route::middleware('throttle:60,1')->group(function () {
Route::get('/users', [UserController::class, 'index']);
Route::get('/posts', [PostController::class, 'index']);
});
// Строгий лимит для auth
Route::middleware('throttle:5,1')->group(function () {
Route::post('/login', [AuthController::class, 'login']);
});

Пользовательские rate limiter’ы

// app/Providers/RouteServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
public function boot()
{
$this->configureRateLimiting();
}
protected function configureRateLimiting()
{
// Лимитер API по умолчанию
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)
->by($request->user()?->id ?: $request->ip());
});
// Многоуровневые лимиты в зависимости от тарифного плана
RateLimiter::for('api-tiered', function (Request $request) {
$user = $request->user();
if (!$user) {
return Limit::perMinute(10)->by($request->ip());
}
return match($user->plan) {
'enterprise' => Limit::none(),
'pro' => Limit::perMinute(1000)->by($user->id),
'basic' => Limit::perMinute(100)->by($user->id),
default => Limit::perMinute(30)->by($user->id),
};
});
// Несколько лимитов
RateLimiter::for('uploads', function (Request $request) {
return [
Limit::perMinute(10), // 10 в минуту
Limit::perHour(100), // 100 в час
Limit::perDay(500), // 500 в день
];
});
}

Применение пользовательских limiter’ов

Route::middleware('throttle:api-tiered')->group(function () {
Route::apiResource('users', UserController::class);
});
Route::middleware('throttle:uploads')->group(function () {
Route::post('/upload', [UploadController::class, 'store']);
});

Ограничение частоты запросов в Nginx

http {
# Define rate limit zones
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
# Status code for rate limited requests
limit_req_status 429;
server {
location /api/ {
# Allow burst of 20, delay after 10
limit_req zone=api burst=20 delay=10;
proxy_pass http://backend;
}
location /api/auth/login {
# Strict: no burst, immediate rejection
limit_req zone=login burst=5 nodelay;
proxy_pass http://backend;
}
}
}

Заголовки ответа

Стандартные заголовки, которые следует включать:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1640000000
Retry-After: 60
// Middleware Express для добавления заголовков
function rateLimitHeaders(req, res, next) {
const limit = 100;
const remaining = req.rateLimit?.remaining ?? limit;
const resetTime = req.rateLimit?.resetTime ?? Date.now() + 60000;
res.set({
'X-RateLimit-Limit': limit,
'X-RateLimit-Remaining': remaining,
'X-RateLimit-Reset': Math.ceil(resetTime / 1000)
});
next();
}

Обработка на стороне клиента

async function apiCall(url, options = {}) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 60;
console.log(`Rate limited. Retry after ${retryAfter} seconds`);
// Экспоненциальная задержка
await new Promise(r => setTimeout(r, retryAfter * 1000));
return apiCall(url, options); // Повторить запрос
}
// Логировать оставшуюся квоту
const remaining = response.headers.get('X-RateLimit-Remaining');
if (remaining && parseInt(remaining) < 10) {
console.warn(`Low API quota: ${remaining} requests remaining`);
}
return response;
}

Лучшие практики

Корректно идентифицируйте пользователей

function getUserIdentifier(req) {
// Предпочитать ID пользователя для аутентифицированных пользователей
if (req.user?.id) {
return `user:${req.user.id}`;
}
// Использовать API key, если предоставлен
const apiKey = req.headers['x-api-key'];
if (apiKey) {
return `apikey:${apiKey}`;
}
// В качестве fallback использовать IP (учитывайте X-Forwarded-For за прокси)
const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.ip;
return `ip:${ip}`;
}

Разные лимиты для разных операций

const rateLimits = {
'GET /api/users': { limit: 1000, window: 60 },
'POST /api/users': { limit: 10, window: 60 },
'DELETE /api/users': { limit: 5, window: 60 },
'POST /api/auth/login': { limit: 5, window: 900 },
'POST /api/upload': { limit: 10, window: 3600 }
};

Плавная деградация

async function handleRequest(req, res) {
const { allowed, remaining } = await checkRateLimit(req);
if (!allowed) {
// Вернуть кэшированный ответ, если доступен
const cached = await cache.get(req.url);
if (cached) {
res.set('X-Served-From-Cache', 'true');
return res.json(cached);
}
return res.status(429).json({
error: 'Rate limit exceeded',
message: 'Please try again later'
});
}
// Обработать в обычном режиме
}

Ключевые выводы

  1. Выберите правильный алгоритм: token bucket для API с допуском всплесков
  2. Используйте Redis для распределённых систем: разделяйте состояние между инстансами
  3. Возвращайте корректные заголовки: помогайте клиентам управлять своей квотой
  4. Разделяйте по пользователю/тарифу: разные лимиты для разных клиентов
  5. Комбинируйте с Nginx: двойная защита на уровне приложения и прокси
  6. Логируйте и мониторьте: отслеживайте срабатывания лимитов для планирования ёмкости

Ограничение частоты запросов — это баланс между защитой и пользовательским опытом: слишком строгие лимиты раздражают пользователей, слишком мягкие — открывают уязвимости. Начните с консервативных значений и корректируйте их на основе реальных паттернов использования.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
Зетка Интерактив