Rate limiting и throttling в JavaScript: Защита и производительность
Вы когда-нибудь задумывались, почему после пары десятков быстрых кликов на кнопку «Отправить» ваш любимый сервис вдруг перестаёт отвечать? Или почему некоторые сайты просят вас «подождать 30 секунд» перед повторной отправкой формы? Ответ кроется в двух мощных механизмах, которые спасают бэкенд от перегрузки, а пользователей — от «багов»: rate limiting и throttling.
В этой статье мы разберём, как применять эти техники на чистом JavaScript и Node.js. Вы узнаете, чем throttle отличается от debounce, как написать простой rate limiter для API и почему ваш сайт будет работать быстрее. Поехали!
Что такое rate limiting и зачем он нужен?
Rate limiting (ограничение скорости запросов) — это защитный механизм, который ограничивает количество запросов от клиента (или IP-адреса) за определённый промежуток времени. Представьте себе шлагбаум: машина может проехать, но только раз в 10 секунд.
Где это применяется?
- Защита API от DDoS-атак и спама.
- Предотвращение перегрузки сервера при массовых запросах.
- Контроль использования ресурсов в платных API (например, 100 запросов в час).
- Защита от ошибок «бесконечного цикла» на клиенте.
В отличие от throttling, который сглаживает поток запросов, rate limiting — это жёсткая стена: «Стоп, ты превысил лимит».
Throttling vs Debounce: главное отличие
Часто путают throttling и debounce. Давайте расставим точки над «i».
- Throttle — гарантирует, что функция будет вызвана не чаще одного раза в N миллисекунд. Представьте водопроводный кран: вы открываете его, и вода течёт с постоянной скоростью, даже если вы дёргаете ручку быстрее.
- Debounce — откладывает вызов функции до тех пор, пока не пройдёт N миллисекунд после последнего события. Идеально для поиска с автодополнением: пока пользователь печатает, запрос не отправляется.
В контексте rate limiting нас интересует именно throttle — мы хотим контролировать частоту, а не откладывать запросы.
Простейший rate limiter на JavaScript
Давайте напишем свою реализацию rate limiter на Node.js. Она будет запоминать время последнего запроса от каждого пользователя и блокировать слишком частые обращения.
class RateLimiter {
constructor(limit, windowMs) {
this.limit = limit; // Максимум запросов
this.windowMs = windowMs; // Временное окно в мс
this.requests = new Map(); // Хранилище: userId -> [timestamps]
}
allow(userId) {
const now = Date.now();
const userRequests = this.requests.get(userId) || [];
// Удаляем устаревшие записи
const recentRequests = userRequests.filter(
ts => now - ts < this.windowMs
);
if (recentRequests.length >= this.limit) {
console.log(`Rate limit exceeded for ${userId}`);
return false;
}
recentRequests.push(now);
this.requests.set(userId, recentRequests);
return true;
}
}
// Использование
const limiter = new RateLimiter(5, 60000); // 5 запросов в минуту
// Имитация запросов
for (let i = 0; i < 10; i++) {
setTimeout(() => {
const allowed = limiter.allow('user123');
console.log(`Request ${i+1}: ${allowed ? '✅ Разрешён' : '❌ Заблокирован'}`);
}, i * 2000); // Каждые 2 секунды
}
Этот код отлично подходит для образовательных целей, но в продакшене лучше использовать готовые решения (например, express-rate-limit для Express или rate-limiter-flexible).
Throttling на клиенте: защита от быстрых кликов
Часто проблема возникает на стороне клиента: пользователь несколько раз нажимает кнопку «Отправить», и запросы уходят в параллель. Решение — throttle на фронтенде.
function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
func.apply(this, args);
}
};
}
// Пример: кнопка отправки
const submitButton = document.getElementById('submit');
const handleSubmit = throttle((event) => {
console.log('Запрос отправлен', event);
// Ваш API-запрос
}, 2000); // Не чаще раза в 2 секунды
submitButton.addEventListener('click', handleSubmit);
Теперь даже если пользователь кликнет 10 раз за секунду, запрос уйдёт только один. Остальные будут проигнорированы.
Продвинутый rate limiter с окнами и блокировкой
В реальном мире нужно не только считать запросы, но и сообщать пользователю, сколько времени осталось до разблокировки. Давайте добавим эту логику.
class AdvancedRateLimiter {
constructor(limit, windowMs, blockDurationMs) {
this.limit = limit;
this.windowMs = windowMs;
this.blockDurationMs = blockDurationMs;
this.store = new Map();
}
check(userId) {
const now = Date.now();
const record = this.store.get(userId);
// Если пользователь заблокирован
if (record && record.blockedUntil > now) {
const timeLeft = Math.ceil((record.blockedUntil - now) / 1000);
return {
allowed: false,
retryAfter: timeLeft,
message: `Слишком много запросов. Подождите ${timeLeft} сек.`
};
}
// Очистка старых записей
const timestamps = (record?.timestamps || []).filter(
ts => now - ts < this.windowMs
);
if (timestamps.length >= this.limit) {
// Блокируем пользователя
this.store.set(userId, {
timestamps,
blockedUntil: now + this.blockDurationMs
});
return {
allowed: false,
retryAfter: Math.ceil(this.blockDurationMs / 1000),
message: `Превышен лимит. Доступ будет восстановлен через ${Math.ceil(this.blockDurationMs / 1000)} сек.`
};
}
timestamps.push(now);
this.store.set(userId, { timestamps });
return { allowed: true, message: 'OK' };
}
}
// Использование
const limiter = new AdvancedRateLimiter(3, 10000, 30000); // 3 запроса за 10 сек, блокировка на 30 сек
// Эмуляция быстрых запросов
[1,2,3,4,5].forEach((_, i) => {
setTimeout(() => {
const result = limiter.check('user456');
console.log(`Запрос ${i+1}: ${result.message}`);
}, i * 2000);
});
Rate limiting на сервере: middleware для Express
Если вы работаете с Express.js, вот пример middleware для ограничения запросов по IP-адресу.
const rateLimitMiddleware = (rateLimiter) => {
return (req, res, next) => {
const userId = req.ip; // Или req.user.id для авторизованных
const result =