# Ограничения частоты запросов

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

## Коротко

Один API-ключ может отправлять **600 чтений** (GET) и **200 записей** (POST, PATCH, DELETE) в минуту. При превышении API отвечает `429` с кодом `request_rate_limited` и заголовком `Retry-After`, в котором указано, сколько секунд подождать. В каждом успешном ответе приходит `X-RateLimit-Remaining` — сколько запросов осталось в текущей минуте.

Но **не всякий 429 — это частота**. Коды `tariff_daily_burst` и `tariff_limit_reached` приходят с тем же HTTP-статусом, но это лимиты тарифа: подождать пару секунд не поможет. Поэтому код должен сначала прочитать поле `error`, а уже потом решать, повторять ли запрос.

## Точные лимиты

| Что | Лимит | Счётчик |
|---|---|---|
| GET (чтение) | 600 в минуту | отдельно на каждый API-ключ |
| POST, PATCH, DELETE (запись) | 200 в минуту | отдельно на каждый API-ключ |
| Публичные запросы страницы оплаты | 120 в минуту | по IP-адресу |

Окно — фиксированная минута: когда минута заканчивается, счётчик обнуляется. Если вы создали два ключа, у каждого свой счётчик — это удобно, когда у вас несколько проектов, но плодить ключи ради обхода лимита не нужно: обычный магазин до этих значений не доходит.

Заголовок `X-RateLimit-Remaining` приходит в каждом успешном ответе. Пишите его в мониторинг — так вы увидите приближение к порогу заранее, а не по факту ошибок.

## Три вида 429

| Код | Откуда приходит | Что случилось | Что делать |
|---|---|---|---|
| `request_rate_limited` | API (`/api/v1/*`) | Превышена минутная частота по ключу | Подождать указанное в `Retry-After` и повторить |
| `rate_limited` | Код входа в кабинет | Код запрошен повторно раньше чем через 60 секунд или исчерпан суточный лимит кодов | Подождать минуту. В ответе есть поле `retryAfterSeconds` |
| `too_many_attempts` | Ввод кода | Слишком много неверных попыток ввода | Запросить новый код, старый вводить бесполезно |

`rate_limited` и `too_many_attempts` относятся к входу в кабинет и в серверной интеграции обычно не встречаются. Если они у вас появились — значит код обращается к эндпоинтам авторизации.

## Как отличить частоту от лимита

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

| Код | Тип | Когда отпустит | Можно ли повторять |
|---|---|---|---|
| `request_rate_limited` | Частота | Через несколько секунд | Да, по `Retry-After` |
| `tariff_daily_burst` | Суточная защита | На следующие сутки | **Нет.** Сначала проверьте код — это обычно интеграция, ушедшая в цикл |
| `tariff_limit_reached` | Месячный лимит тарифа | В новом месяце или после повышения тарифа | **Нет.** Кабинет → Тариф |

Слепой ретрай опасен именно здесь: получив `tariff_daily_burst`, цикл не остановится и будет часами долбить API. Подробнее: [Тарифы и лимиты](/kb/ru/tariff-limits).

## Заголовок Retry-After

В ответе `request_rate_limited` заголовок `Retry-After` приходит в секундах — это время до конца текущего минутного окна. Прочитать его и подождать ровно столько — самая правильная тактика: если повторить раньше, снова получите 429.

Если заголовка нет (например, его срезал прокси), переходите на бэкофф: 1 секунда, дальше удвоение.

## Как обработать это в коде

Правило простое: **читайте поле `error` и решайте по нему.**

```js
const RETRYABLE = new Set(['request_rate_limited', 'rate_limited']);

async function callWithRetry(fn, { maxAttempts = 5 } = {}) {
  const delays = [1000, 2000, 4000, 8000]; // 1, 2, 4, 8 секунд
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      // Лимит тарифа — ожидание не поможет, выходим сразу
      if (err.code === 'tariff_limit_reached' || err.code === 'tariff_daily_burst') throw err;
      if (!RETRYABLE.has(err.code) && !(err.status >= 500)) throw err;
      if (attempt === maxAttempts - 1) throw err;

      const retryAfter = Number(err.retryAfterSeconds || 0) * 1000;
      const wait = retryAfter || delays[Math.min(attempt, delays.length - 1)];
      await new Promise((r) => setTimeout(r, wait + Math.random() * 300));
    }
  }
}
```

Случайная добавка в конце (`Math.random()`) — это джиттер. Он нужен, чтобы несколько параллельных процессов не проснулись в одну и ту же миллисекунду и не получили 429 снова.

Повторяя создание счёта, **всегда передавайте тот же `Idempotency-Key`** — иначе может оказаться, что первый запрос всё-таки прошёл, и повтор создаст второй счёт. Подробнее: [Идемпотентность](/kb/ru/idempotency).

## Как вообще не упираться в частоту

| Сценарий | Правильный подход |
|---|---|
| Выставить много счетов разом | `POST /api/v1/invoices/bulk` — до 100 счетов за один запрос: [Массовое создание счетов](/kb/ru/bulk-invoices) |
| Ждать оплату | Вебхук как основной канал, опрос только первые 30 секунд: [Вебхук или опрос статуса](/kb/ru/polling-vs-webhook) |
| Периодически сверять все счета | Берите список `GET /invoices` с фильтрами, а не каждый счёт отдельным запросом |
| Отчётность | Один экспорт в сутки вместо запроса на каждую строку |

Самая частая причина упора в лимит — цикл с `GET /invoices/{id}` без условия выхода. Пропишите условие остановки в каждом таком цикле.

## Вопросы и ответы

**Получил 429, но заголовка `Retry-After` нет.** Значит это не `request_rate_limited`, а код тарифа. Смотрите поле `error`.

**Частота влияет на количество счетов?** Нет. Частота — это запросы в минуту, а лимит тарифа — счета за месяц. Считаются независимо.

**Можно ли поднять лимит?** Стандартных значений хватает при обычной нагрузке. Если ваш сценарий в них не укладывается, напишите в [поддержку](https://qut.kz/app/support).

**В песочнице лимиты другие?** Частота такая же. А лимиты тарифа на счета песочницы не распространяются.

**Несколько серверов работают с одним ключом.** Счётчик общий на ключ, поэтому лимит делится между всеми серверами. Если нагрузку нужно развести, выдайте каждому сервису свой ключ.
