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 в живом поиске — сотни запросов при быстром вводе
  • Несколько вкладок/воркеров без координации — превышают совокупный лимит

Связанные темы

Ресурсы