Ограничение скорости API — заголовки, экспоненциальный откат и выживание перед 429

Обновлено

Вы получили код 429. API сообщает вам о необходимости снижения скорости. Вот как расшифровать заголовки X-RateLimit-*, понять значение Retry-After и реализовать экспоненциальный откат с добавлением случайного сдвига, чтобы ваши интеграции корректно обрабатывали ограничения скорости, а не наносили удары по серверу.

Ограничение скорости API — заголовки, экспоненциальный откат и выживание 429 1
Реклама · УДАЛИТЬ?

Вы получили код 429. Возможно, ваш обработчик веб-хука перестал работать. Возможно, батч-задача была проигнорирована. API вернул сообщение «Слишком много запросов» и поток ответных заголовков, которые вы, вероятно, пропустили.

Эти заголовки содержат всю информацию. Вот как их читать и как написать логику повторных попыток, которая не усугубляет проблему.

Заголовки, которые действительно важны

Большинство API, ограничивающих количество запросов, возвращают какие-то из этих заголовков на каждом ответе — не только при коде 429:

  • X-RateLimit-Limit — общее количество раз, которое вы можете сделать в текущем окне. REST-интерфейс GitHub предоставляет авторизованным пользователям 5 000 запросов в час; неавторизованные запросы ограничены 60.
  • X-RateLimit-Remaining — количество оставшихся запросов в текущем окне. Когда этот показатель достигает нуля, следующий запрос возвращает код 429.
  • X-RateLimit-Reset — время сброса окна, в формате Unix-эпохи. Это тот заголовок, который чаще всего игнорируют разработчики, и при этом наиболее полезный.
  • X-RateLimit-Used (специфичный для GitHub) — количество уже использованных запросов. Отражает Limit - Remaining но полезен для проверки состояния.
  • Retry-After — появляется только в ответах с кодом 429. Может быть указано как количество секунд ожидания или как строка даты в формате HTTP. Если API отправляет этот заголовок, используйте его — он точнее, чем любое вычисление, которое вы можете сделать самостоятельно.

Реальные заголовки ответа GitHub выглядят так:

X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4823
X-RateLimit-Reset: 1716998400
X-RateLimit-Used: 177
X-RateLimit-Resource: core

The X-RateLimit-Resource заголовок специфичен для GitHub: они поддерживают отдельные пулы квот для основного REST-интерфейса, поиска и GraphQL. Использование квоты поиска (ограничено до 30 запросов в минуту) не влияет на основные квоты — и наоборот.

Stripe отличается

от X-RateLimit-* названия. Их заголовки имеют другой префикс:

Stripe-Ratelimit-Limit: 100
Stripe-Ratelimit-Remaining: 97
Stripe-Ratelimit-Reset: 1716998460

И на ответе с кодом 429:

Retry-After: 30

по умолчанию у Stripe ограничение составляет 100 активных запросов в секунду, а не в час. Это важно, чем кажется: цикл импорта 500 клиентов может исчерпать этот диапазон менее чем за 5 секунд, если вы не ограничиваете запросы на вашем конце.

Stripe также различает ограничения по количеству запросов и ограничения по конкретным ресурсам (например, создание слишком большого количества клиентов в коротком промежутке времени). В теле ответа с кодом 429 указано, какой именно лимит был превышен — всегда логируйте полное тело ответа, а не только код статуса.

Декодирование времени сброса

The X-RateLimit-Reset значение — это Unix-эпоха. 1716998400 показывает ничего несущественное на первый взгляд, но его легко декодировать: используйте Конвертер временных меток Unix для преобразования в понятную дату UTC и определения, насколько далеко до сброса.

В коде: reset_time - time.now() показывает количество секунд до сброса. Но проверьте X-RateLimit-Remaining сначала — если у вас ещё есть квота, то ничего не нужно ждать.

Что сообщает тело ответа с кодом 429

Код 429 сам по себе недостаточен. Тело ответа обычно указывает, какой именно лимит был превышен:

GitHub:

{
  "message": "API rate limit exceeded for user ID 12345.",
  "documentation_url": "https://docs.github.com/rest/overview/rate-limits"
}

Stripe:

{
  "error": {
    "code": "rate_limit",
    "message": "Too many requests hit the API too quickly.",
    "type": "invalid_request_error"
  }
}

OpenAI идёт дальше: в сообщении об ошибке указано, какой именно лимит был превышен — либо по токенам в минуту, либо по запросам в минуту, что кардинально меняет стратегию повторных попыток. Всегда логируйте полное тело ответа с кодом 429.

Экспоненциальный откат с дрожанием

Простое решение: перехватить код 429, поставить паузу на 1 секунду и повторить запрос. Это не сработает по двум причинам:

  • Если у вас несколько рабочих процессов, которые обращаются к одному и тому же конечному пункту, они все будут ждать 1 секунду и повторять запрос одновременно — это синхронизированный поток повторных попыток, который воссоздаёт саму проблему.
  • 1 секунда бесполезна, если вы исчерпали квоту на час или на день. Вы просто соберёте ещё 3600 ошибок 429.

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

import time
import random
import requests

def fetch_with_backoff(url, headers, max_retries=5):
    base_delay = 1  # seconds

    for attempt in range(max_retries):
        response = requests.get(url, headers=headers)

        if response.status_code != 429:
            return response

        # Prefer Retry-After if the API provides it
        retry_after = response.headers.get("Retry-After")
        if retry_after:
            wait = int(retry_after)
        else:
            # Fall back to X-RateLimit-Reset
            reset = response.headers.get("X-RateLimit-Reset")
            if reset:
                wait = max(0, int(reset) - int(time.time()))
            else:
                # Pure exponential backoff with full jitter
                cap = 60  # max wait: 60s
                wait = random.uniform(0, min(cap, base_delay * (2 ** attempt)))

        print(f"Rate limited. Attempt {attempt + 1}/{max_retries}. Waiting {wait:.1f}s")
        time.sleep(wait)

    raise Exception(f"Max retries exceeded after {max_retries} attempts")

Приоритет в этом реализации умышленно установлен следующим образом:

  • Retry-After первым — если API указывает, сколько секунд нужно ждать, используйте это. Не угадывайте с собственным расчётом.
  • X-RateLimit-Reset как альтернатива — рассчитайте реальное количество секунд до сброса, а не предполагайте фиксированный интервал.
  • Полное дрожание как последний вариантrandom.uniform(0, cap) распределяет попытки по всему интервалу отката. В блоге по архитектуре AWS описано это как «полное дрожание» и показано, что оно значительно снижает коллизии на сервере по сравнению с равномерным дрожанием или отсутствием дрожания вовсе.
  • max(0, ...) при сбросе — время сброса может быть в прошлом к моменту, когда вы производите расчёты. Защититесь от отрицательного значения времени ожидания, которое может вызвать сбой вашего обработчика.

Общие ошибки

Рассматривание ошибок, не являющихся 429, как ошибок лимита запросов. Код 503 — это ошибка сервера. Код 401 означает, что ваши данные неверны. Проверьте status_code == 429 до применения логики повторных попыток по лимиту запросов.

Поглощение ошибки 429 и возврат пустых данных. Скрытые сбои сложнее отладить, чем исключения, которые были выброшены. Показывайте ошибку.

Использование фиксированного интервала. Если вы исчерпали квоту на час, и у вас осталось 47 минут, то ожидание 5 секунд не принесёт вам ничего. Рассчитывайте на основе времени сброса.

Повторные попытки без предела. Установите max_retries установите лимит и увеличьте после того, как квота исчерпана. Некоторые ошибки 429 указывают на исчерпание квоты, которое не восстанавливается до следующего периода оплаты — бесконечный цикл повторных попыток — это баг.

Не отслеживание X-RateLimit-Remaining заранее. Если Remaining падает ниже 10% из Limit, начните распределять запросы до того, как вы достигнете нуля. Большинство SDK не делают это автоматически. Стоимость — несколько миллисекунд дополнительной задержки; выгоды — вы не увидите ошибки 429 вообще.

Подводя итог

Ошибка 429 — это не одноразовая проблема, которую можно исправить и забыть. Это повторяющаяся ограничение, и игнорирование заголовков, которые с ней идут, означает, что вы будете постоянно сталкиваться с тем же барьером. Используйте Retry-After при наличии в API. Рассчитывайте на основе X-RateLimit-Reset при отсутствии. Добавьте дрожание, чтобы повторные попытки не синхронизировались. Установите лимит, чтобы бесконечные циклы повторных попыток не превращались в инциденты в продакшене.

И когда вы смотрите на X-RateLimit-Reset: 1716998400 и спрашиваете, когда это на самом деле происходит — Конвертер временных меток Unix покажет это за один клик.

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

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

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

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

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

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

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

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

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

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

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