> Как спроектировать и реализовать rate limiting для платного API с разными тарифами (JavaScript)

Уровень: senior · Роль: frontend · Язык: JavaScript · Категория: Технические вопросы

Компании: QueenInteractiveGamesLtd

Стек: Node.js, JavaScript

> Пример ответа

Короткий ответ

Rate limiting для платного API с разными тарифами реализуется через middleware, который проверяет идентификатор клиента (API key или JWT), определяет его тарифный план и применяет соответствующие лимиты (requests per minute/hour/day). Данные о потреблении хранятся в Redis с TTL, равным окну лимита. При превышении возвращается 429 Too Many Requests с заголовками Retry-After и X-RateLimit-*.

Подробное объяснение

Архитектура rate limiting для платного API включает несколько ключевых компонентов:

  1. Идентификация клиента - каждый запрос содержит API key или JWT, по которому определяется тарифный план (free, pro, enterprise).

  2. Хранение счетчиков - Redis идеально подходит благодаря атомарным операциям (INCR, EXPIRE) и встроенной поддержке TTL. Для sliding window используется sorted sets или Lua-скрипты.

  3. Алгоритмы:

    • Fixed window - простой, но допускает burst-запросы на границах окна
    • Sliding window - более точный, использует sorted set с временными метками
    • Token bucket - гибкий, позволяет накапливать неиспользованные запросы
    • Leaky bucket - сглаживает burst, но не подходит для накопления
  4. Тарифные планы - хранятся в конфигурации или базе данных, содержат лимиты на разные временные окна (10 req/s, 1000 req/h, 10000 req/day).

  5. Ответ при превышении - HTTP 429 с заголовками:

    • X-RateLimit-Limit - общий лимит
    • X-RateLimit-Remaining - оставшиеся запросы
    • X-RateLimit-Reset - время сброса (Unix timestamp)
    • Retry-After - секунды до следующего разрешенного запроса
  6. Graceful degradation - при недоступности Redis можно временно пропускать запросы (fail-open) или блокировать (fail-closed), в зависимости от SLA.

На практике

Для Node.js с Express типичная реализация:

  1. Создаем middleware, который извлекает API key из заголовка Authorization или параметра запроса.

  2. По API key получаем тарифный план (из кэша или БД). Для ускорения используем in-memory cache с коротким TTL.

  3. Формируем ключ для Redis: ratelimit:{plan}:{apiKey}:{window}.

  4. Используем Lua-скрипт для атомарного инкремента и проверки лимита:

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

-- Удаляем старые записи (для sliding window)
redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)

-- Получаем текущее количество
local count = redis.call('ZCARD', key)

if count >= limit then
    return {0, count, redis.call('TTL', key)}
end

-- Добавляем новый запрос
redis.call('ZADD', key, now, now .. ':' .. math.random())
redis.call('EXPIRE', key, window)
return {1, count + 1, window}
  1. В ответе добавляем заголовки с информацией о лимите.

  2. Для enterprise-клиентов можно реализовать soft limit - предупреждение при 80% использования, hard limit при 100%.

Пример кода

JAVASCRIPT
const redis = require('redis');
const client = redis.createClient();
const RATE_LIMIT_SCRIPT = `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)
local count = redis.call('ZCARD', key)
if count >= limit then
local ttl = redis.call('TTL', key)
return {0, count, ttl}
end
redis.call('ZADD', key, now, now .. ':' .. math.random())
redis.call('EXPIRE', key, window)
return {1, count + 1, window}
`;
async function rateLimiter(req, res, next) {
const apiKey = req.headers['x-api-key'];
if (!apiKey) return res.status(401).json({ error: 'API key required' });
const plan = await getPlan(apiKey); // из кэша или БД
const limits = PLANS[plan]; // { requestsPerSecond: 10, requestsPerHour: 1000 }
const now = Date.now();
const windowMs = 1000; // 1 second window
const key = `ratelimit:${plan}:${apiKey}:second`;
const result = await client.eval(RATE_LIMIT_SCRIPT, 1, key, limits.requestsPerSecond, windowMs / 1000, now);
const [allowed, remaining, resetIn] = result;
res.set({
'X-RateLimit-Limit': limits.requestsPerSecond,
'X-RateLimit-Remaining': Math.max(0, limits.requestsPerSecond - remaining),
'X-RateLimit-Reset': Math.ceil((now + resetIn * 1000) / 1000),
'Retry-After': resetIn
});
if (!allowed) {
return res.status(429).json({ error: 'Too many requests' });
}
next();
}

Как отвечать на собеседовании

  1. Начни с общей архитектуры: middleware, Redis, тарифные планы.
  2. Объясни выбор алгоритма - для платного API лучше sliding window или token bucket.
  3. Упомяни заголовки ответа и graceful degradation.
  4. Покажи понимание trade-off: точность vs производительность, стоимость Redis-операций.
  5. Добавь про мониторинг - метрики в Prometheus, алерты при превышении лимитов.
  6. Если спросят про распределенные системы - расскажи про консистентность и атомарность Lua-скриптов.

Что проверяет интервьюер

  • Понимание алгоритмов rate limiting и их применимости
  • Умение проектировать масштабируемые решения с Redis
  • Знание HTTP-протокола и заголовков для rate limiting
  • Навыки работы с Lua-скриптами для атомарных операций
  • Понимание бизнес-логики: разные тарифы, soft/hard limits
  • Умение учитывать edge cases: недоступность Redis, burst-запросы

Типичные ошибки

  • Использование fixed window без учета burst на границах
  • Хранение счетчиков в памяти процесса (не масштабируется)
  • Отсутствие graceful degradation при недоступности Redis
  • Неправильный расчет TTL для sliding window
  • Игнорирование заголовков Retry-After и X-RateLimit-*
  • Блокировка всех запросов при превышении лимита одним клиентом
  • Отсутствие кэширования тарифных планов (каждый запрос в БД)

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью