Rate Limiting: клиентская сторона
Rate limiting на клиенте — ограничение частоты исходящих запросов, чтобы не превышать лимиты API и не перегружать сервер.
Зачем нужно
Большинство публичных API (GitHub, Twitter, OpenAI) ограничивают количество запросов в единицу времени. Превышение лимита возвращает 429 Too Many Requests. Клиентский rate limiting предотвращает блокировку, равномерно распределяет нагрузку и позволяет корректно обрабатывать лимиты через заголовки ответа.
Где используется
- Парсинг/скрапинг: ограничение запросов к сайтам
- Интеграция со сторонними API (Stripe, Twilio, SendGrid)
- Живой поиск с debounce — уменьшение числа запросов при вводе
- Пакетная обработка данных с контролем скорости
Чтение заголовков rate limit
# Стандартные заголовки (нет единого стандарта, но часто встречаются)
HTTP/1.1 200 OK
X-RateLimit-Limit: 100 # максимум запросов в окне
X-RateLimit-Remaining: 42 # осталось в текущем окне
X-RateLimit-Reset: 1712345678 # Unix timestamp сброса окна
# При превышении
HTTP/1.1 429 Too Many Requests
Retry-After: 30 # подождать 30 секунд
async function apiRequest(url) {
const res = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}` } });
// Читаем заголовки лимита
const remaining = parseInt(res.headers.get('X-RateLimit-Remaining') || '99');
const reset = parseInt(res.headers.get('X-RateLimit-Reset') || '0');
if (res.status === 429) {
const retryAfter = parseInt(res.headers.get('Retry-After') || '60');
console.warn(`Rate limit hit. Wait ${retryAfter}s`);
await sleep(retryAfter * 1000);
return apiRequest(url); // повтор после ожидания
}
// Предупреждение при приближении к лимиту
if (remaining < 10) {
const waitMs = (reset * 1000) - Date.now();
if (waitMs > 0) await sleep(waitMs);
}
return res.json();
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
Throttle — контроль частоты запросов
// Очередь с ограничением скорости
class RateLimitedQueue {
constructor(ratePerSecond = 5) {
this.queue = ;
this.interval = 1000 / ratePerSecond;
this.running = false;
}
enqueue(fn) {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject });
if (!this.running) this.run;
});
}
async run {
this.running = true;
while (this.queue.length > 0) {
const { fn, resolve, reject } = this.queue.shift();
try { resolve(await fn); } catch (e) { reject(e); }
await new Promise(r => setTimeout(r, this.interval));
}
this.running = false;
}
}
const queue = new RateLimitedQueue(5); // 5 запросов/сек
// Использование
const urls = Array.from({ length: 50 }, (_, i) => `/api/item/${i}`);
const results = await Promise.all(
urls.map(url => queue.enqueue( => fetch(url).then(r => r.json())))
);
Debounce для живого поиска
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout( => fn(...args), delay);
};
}
const search = debounce(async (query) => {
const data = await fetch(`/api/search?q=${query}`).then(r => r.json());
renderResults(data);
}, 300); // ждём 300ms паузы в вводе
input.addEventListener('input', e => search(e.target.value));
Частые ошибки
- Игнорирование заголовков
Retry-After— повторный запрос сразу после 429 - Линейное ожидание вместо экспоненциального backoff при повторных 429
- Отсутствие debounce в живом поиске — сотни запросов при быстром вводе
- Несколько вкладок/воркеров без координации — превышают совокупный лимит