Ограничение скорости Как прекратить получать ошибку 429 от каждой API-сервиса, с которым вы взаимодействуете

Опубликовано

HTTP 429 слишком много запросов — как читать заголовки Retry-After, реализовывать экспоненциальный откат с дрожанием, понимать алгоритмы бакета токенов и бакета утечки, и реализовывать ограничение скорости на собственной API.

Ограничение скорости: Как прекратить получать ошибку 429 от каждой API-сервиса, с которым вы взаимодействуете 1
Реклама · УДАЛИТЬ?

Вы получили код 429. Ваш скрипт активно обращается к API в течение последнего часа, ваши логи полны красных сообщений, и развертывание запланировано на 20 минут. В этот момент абстракция прекращается.

Код HTTP 429 «Слишком много запросов» означает, что вы отправили больше запросов, чем разрешено сервером в заданном временном интервале. Правильное чтение ответа и корректная попытка повтора — это навык, который большинство разработчиков приобретают только после того, как им пришлось столкнуться с этой проблемой. Вот полная картина.

То, что на самом деле сообщает 429

Код состояния — это только половина сообщения. Реальная информация содержится в заголовках:

  • Retry-After: 30 — ждите 30 секунд перед повторной попыткой. Также может быть указан HTTP-дата: Retry-After: Mon, 08 Jun 2026 15:00:00 GMT
  • X-RateLimit-Limit: 100 — общее количество запросов, разрешённых в интервале
  • X-RateLimit-Remaining: 0 — количество оставшихся запросов в текущем интервале (вы достигли нуля)
  • X-RateLimit-Reset: 1749391200 — Unix-временная метка, когда интервал обновляется

Не все API отправляют все эти данные. GitHub отправляет полный набор. Stripe отправляет Retry-After. Некоторые REST-API ничего не отправляют и ожидает, что вы догадаетесь. Если вы имеете Retry-After, используйте его точно — это то, что сервер сообщает о минимально безопасном времени ожидания. Если вы не используете это, то экспоненциальный откат становится вашим резервным решением.

Неправильный способ повторной попытки

Простая реализация выглядит так:

async function fetchWithoutBackoff(url) {
  while (true) {
    const res = await fetch(url);
    if (res.ok) return res;
    if (res.status === 429) continue; // immediately retry
  }
}

Это активно вредит. Если 10 экземпляров вашей службы одновременно сталкиваются с кодом 429 и сразу начинают повторные попытки, все повторные попытки приходят в одно и то же время — проблема «гурманской стадии». Вы снова сталкиваетесь с ограничением скорости, немедленно, в бесконечном цикле, который может продолжаться и заставляет ваш клиент выглядеть как будто он намеренно злоупотребляет API.

Экспоненциальный откат с добавлением случайного сдвига

Правильный паттерн: каждая попытка повтора ждёт дольше предыдущей (экспоненциальный), а случайный сдвиг предотвращает синхронизацию повторных попыток между несколькими клиентами (сдвиг).

async function fetchWithBackoff(url, options = {}, maxRetries = 5) {
  let attempt = 0;

  while (attempt <= maxRetries) {
    const res = await fetch(url, options);

    if (res.ok) return res;

    if (res.status !== 429) {
      throw new Error(`Request failed: ${res.status}`);
    }

    if (attempt === maxRetries) {
      throw new Error(`Rate limited after ${maxRetries} retries`);
    }

    // Use Retry-After if provided; otherwise exponential backoff + jitter
    const retryAfter = res.headers.get('Retry-After');
    let waitMs;

    if (retryAfter) {
      const seconds = isNaN(retryAfter)
        ? (new Date(retryAfter) - Date.now()) / 1000  // HTTP date
        : Number(retryAfter);                          // seconds
      waitMs = seconds * 1000;
    } else {
      const baseDelay = 1000 * Math.pow(2, attempt); // 1s, 2s, 4s, 8s, 16s
      const jitter = Math.random() * 1000;            // 0–1000ms random offset
      waitMs = baseDelay + jitter;
    }

    console.log(`Rate limited. Waiting ${Math.round(waitMs / 1000)}s (attempt ${attempt + 1}/${maxRetries})`);
    await new Promise(resolve => setTimeout(resolve, waitMs));
    attempt++;
  }
}

Линия сдвига — это часть, которую большинство реализаций пропускают. Без неё повторные попытки из нескольких параллельных процессов всё ещё приходят в кластерах. С её использованием они распределяются по временному интервалу ожидания.

Для API, которые возвращают Retry-After, используйте это значение как floor — если вы всё ещё получаете 429 после указанного ожидания, примените экспоненциальный откат на верхнем уровне.

Бакет токенов против бакета с утечкой

Два алгоритма доминируют в реализации ограничителей скорости. Понимание того, какой из них используется, даёт понимание того, как поведёт себя API при высокой нагрузке — и какой алгоритм следует использовать при создании собственного.

Бакет токенов

Бакет может содержать до N токенов. Каждый запрос стоит один токен. Токены пополняются по постоянному темпу (например, 10 в секунду). Если бакет пуст, запрос отклоняется или добавляется в очередь.

Поддерживает всплески. Если вы не делали запросов в течение длительного времени, вы накопили токены и можете отправить всплеск без нарушения лимита. API GitHub работает по этому принципу — 5 000 запросов в час, но вы можете использовать их одновременно, если не обращались к API в течение нескольких часов. Хорошо подходит для интерактивных сценариев с резкими пиками нагрузки.

Бакет с утечкой

Запросы попадают в очередь и постепенно вытекают с постоянной скоростью, независимо от того, насколько быстро они приходят. Если очередь заполнена, входящие запросы отбрасываются.

Плавный выход, без всплесков. Даже если у вас ещё есть квота, запросы поступают с установленной скоростью. Модуль Nginx limit_req использует этот подход. Лучше подходит для защиты нижестоящих систем от всплесков — полезно для доставки вебхуков, внешних API-вызовов и любых сценариев, где важна предсказуемая пропускная способность, а не выносливость к всплескам.

Как выбрать при реализации собственного: Пользовательские конечные точки, требующие всплесков — бакет токенов. Доставка вебхуков или вызовы внешних API — бакет с утечкой. Задачи в фоновом режиме, где важна плавная пропускная способность — бакет с утечкой.

Расчёт безопасных скоростей запросов

Перед написанием любой логики повторных попыток определите, сколько вы на самом деле можете делать. Если API говорит «1 000 запросов в час», это 16,67 запросов в минуту или 0,278 запроса в секунду. Добавьте буфер безопасности 20% и вы получите около 13 запросов в минуту — достаточно пространства для избежания проблем с временным перекрытием, когда два интервала пересекаются.

Используйте Калькулятор ограничений скорости для преобразования квот в скорости в секунду и минуту, определения правильного интервала ожидания между запросами и понимания того, как уровень параллельности влияет на риск всплесков.

Реализация ограничения скорости на собственном API

Если вы находитесь с другой стороны и хотите добавить правильное поведение 429 на своё собственное API:

  1. Выберите подходящую гранулярность. По IP-адресу легко, но не работает для сервисов, находящихся за NAT или с общим выходом. По API-ключу лучше, но требует аутентификации. По идентификатору пользователя — идеально, если у вас есть такой идентификатор. Не смешивайте гранулярности без понимания того, какой из них будет лучше.
  2. Всегда возвращайте Retry-After. Код 429 без Retry-After вынуждает каждый клиент реализовывать собственный алгоритм отката. Вы получите больше проблем «гурманской стадии», а не меньше.
  3. Используйте Redis для распределённого ограничения скорости. Счётчики в памяти не работают между несколькими экземплярами сервера. Redis INCR + EXPIRE является стандартным решением. Библиотеки, такие как rate-limiter-flexible (Node) и slowapi (Python/FastAPI) правильно абстрагируют этот механизм.
  4. Логируйте каждый 429, который вы отправляете. Резкий рост 429-событий от одного ключа означает либо баг клиента, либо намеренное злоупотребление. Оба случая требуют своевременного знания.
  5. Не ограничивайте по ошибкам аутентификации. Возвращайте 401 при неверных учётных данных, а не 429. Ограничение по ошибкам аутентификации — это то, как вы случайно блокируете своих пользователей во время смены учётных данных.

Что делать прямо сейчас

Если вы сталкиваетесь с 429:

  • Проверьте Retry-After первое — используйте его, если оно есть, не изобретайте собственную задержку
  • Реализуйте экспоненциальный откат с добавлением сдвига — код выше готов к копированию и вставке
  • Запишите заголовок X-RateLimit-Remaining в каждом ответе — вы можете быть неоправданно расходуете квоту
  • Кэшируйте ответы, когда данные не меняются часто

Если вы реализуете ограничение скорости: выбирайте библиотеку с поддержкой Redis, возвращайте Retry-After на каждый 429, мониторьте количество 429 по ключу и не ограничивайте по ошибкам аутентификации.

Код 429 не враг — это API, который сообщает вам ровно то, что пошло не так и (обычно) сколько нужно ждать. Большинство проблем с ограничением скорости возникают из-за игнорирования этого сообщения и немедленного повтора попыток. Не делайте этого.

Хотите убрать рекламу? Откажитесь от рекламы сегодня

Установите наши расширения

Добавьте инструменты ввода-вывода в свой любимый браузер для мгновенного доступа и более быстрого поиска

в Расширение Chrome в Расширение края в Расширение Firefox в Расширение Opera

Табло результатов прибыло!

Табло результатов — это интересный способ следить за вашими играми, все данные хранятся в вашем браузере. Скоро появятся новые функции!

Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?

новости с техническими моментами

Примите участие

Помогите нам продолжать предоставлять ценные бесплатные инструменты

Купи мне кофе
Реклама · УДАЛИТЬ?