← Explainer
Explainer · rate limiting

Почему API отвечает 429?

Почти любой лимит запросов внутри устроен как ведро с жетонами — token bucket. Мы собрали его в железе: магазин монет, часы пополнения и лента запросов. Трогайте машину, смотрите ролик — и через пять минут правило b + r·T будет очевидным.

01Машина

Token bucket в железе

Магазин на 5 монет — ведро (ёмкость b). Часы бункера роняют монету раз в 2 секунды — пополнение (скорость r). Каждый запрос на ленте забирает одну монету; нет монеты — толкатель сбрасывает его в лоток 429.

пополнение
5 / 5жетонов в ведре
0пропущено · 200
0отказ · 429
Числа на счётчиках — из той же модели, что двигает детали: монета падает раз в период, лишняя уходит в стакан переполнения, запрос без монеты получает 429. Попробуйте всплеск при полном ведре, а потом — при пустом.
02Ролик · 2:24

Вера объясняет, Макс спрашивает

Диалог над той же машиной: ведро, пополнение, запрос, всплеск, среднее и две ручки лимита. Есть вертикальная версия для Reels/Shorts.

формат
Живой плеер ↗
03Суть

Четыре факта о token bucket

b · ёмкость

Всплеск — не больше b

Полное ведро пропускает b запросов подряд, сколько бы их ни пришло разом.

r · скорость

В среднем — не больше r

Когда запас кончился, проходит ровно столько, сколько капает: r запросов в секунду.

переполнение

Копить нельзя

Ведро не растёт: лишние жетоны сгорают. Долгое молчание не даёт права на огромный всплеск.

429

Отказ — это подсказка

Получили 429 — подождите Retry-After секунд: к этому времени капнет новый жетон.

за любые T секунд пройдёт не больше b + r·T запросовВ нашей машине b = 5, r = 0,5/с: за 10 секунд — не больше 5 + 0,5·10 = 10 запросов.
04Код

Тот же алгоритм — двенадцать строк

В настоящих серверах жетоны не падают по одному: их число пересчитывают при каждом запросе по прошедшему времени. Результат тот же, что у машины.

class TokenBucket {
  constructor(b, r) { this.b = b; this.r = r; this.tokens = b; this.last = Date.now() / 1000; }
  take() {
    const now = Date.now() / 1000;
    // пополнение: r жетонов в секунду, но не больше ёмкости b
    this.tokens = Math.min(this.b, this.tokens + (now - this.last) * this.r);
    this.last = now;
    if (this.tokens >= 1) { this.tokens -= 1; return { ok: true }; }             // 200
    return { ok: false, retryAfter: Math.ceil((1 - this.tokens) / this.r) };  // 429
  }
}

Сделано на anatomy (изометрическая фигура) и video-kit (ролик с озвучкой). Машина — figure.js, модель — model.js, сценарий — video/SCENARIO.md.

Explainer · github.com/nemilya/explainer