Content API щедрый, но не бездонный. Несколько простых лимитов держат его быстрым для всех, а сайт, который разумно кэширует, с ними просто не встретится. Считайте эту страницу знаками ограничения скорости на дороге, по которой вы почти всегда едете гораздо медленнее.
Сами лимиты
| Что считается | Лимит | Что будет при превышении |
|---|---|---|
| Запросы с одного IP-адреса | 300 в минуту | Этот IP получает 429 до конца минуты. |
| Запросы с одним ключом сайта (со всех IP вместе) | 600 в минуту | Запросы с этим ключом получают 429 до конца минуты. |
| Запросы с неверным ключом с одного IP | 20 в минуту | IP блокируется до конца минуты — даже запросы с правильным ключом получают 429. |
Фиксированные окна по минуте
Счётчики работают фиксированными окнами по одной минуте, а не скользящим средним. Запросы набивают счётчик, минута заканчивается — он начинается с нуля. Отсюда два практических вывода:
- Всплеск в самом начале минуты — не беда, если за всю минуту вы уложились в лимит.
- Если уже пришёл
429, в эту минуту ничего не поможет. Поможет следующая.
Какой лимит вы встретите первым
Зависит от того, откуда идут запросы:
- Серверный рендеринг на одной машине. Все запросы уходят с IP вашего сервера, так что первым потолком станут 300 в минуту на IP. У нескольких серверов у каждого свои 300, но вместе они всё равно делят 600 на ключ.
- Запросы из браузера. У каждого посетителя свой IP, поэтому лимит на IP почти никогда не мешает. Зато все посетители ходят с одним ключом сайта, а у ключа 600 запросов в минуту на весь сайт. Кэш браузера помогает каждому посетителю по отдельности, но не толпе.
Как выглядит ответ 429
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Cache-Control: no-store
{"message":"rate_limit_exceeded"}Вот и всё. Заголовка Retry-After нет, заголовков RateLimit-* тоже — не ищите. Раз окна — это ровные минуты, правило простое: подождите около минуты (с небольшой случайной добавкой, о ней ниже) и повторите запрос.
Повтор с паузой и случайным разбросом
Небольшая обёртка над fetch, которая на каждую беду реагирует по-своему:
- 429 — пересидеть окно: около 60 секунд плюс несколько случайных.
- 5xx и сетевые ошибки — экспоненциальная пауза со случайным разбросом: примерно 0,5–1 с, потом 1–2 с, потом 2–4 с.
- Любые другие 4xx — никаких повторов. Опечатку в маркере ожиданием не исправить.
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
type RetryOptions = {
retries?: number; // сколько дополнительных попыток
maxWaitMs?: number; // дольше этого не ждём — сразу отдаём ошибку
};
export async function fetchWithRetry(
url: string,
init: RequestInit = {},
{ retries = 3, maxWaitMs = 70_000 }: RetryOptions = {},
): Promise<Response> {
for (let attempt = 0; ; attempt++) {
let res: Response | undefined;
let error: unknown;
try {
res = await fetch(url, init);
// 2xx, 304 или 4xx, который ожиданием не лечится (400, 401, 404…): отдаём как есть
if (res.status !== 429 && res.status < 500) return res;
} catch (err) {
error = err; // беда с сетью: DNS, таймаут, оборванное соединение
}
const wait =
res?.status === 429
? 60_000 + Math.random() * 5_000 // окно — минута: пересиживаем её плюс разброс
: 2 ** attempt * 500 * (1 + Math.random()); // 5xx / сеть: 0,5–1 с, 1–2 с, 2–4 с…
if (attempt >= retries || wait > maxWaitMs) {
if (res) return res; // пусть вызывающий код увидит 429 / 5xx
throw error;
}
await sleep(wait);
}
}Там, где никто не ждёт, вызывайте её терпеливо, а там, где ждёт человек, — нетерпеливо:
const headers = { 'x-crm-key': process.env.CRM_KEY! };
const url = 'https://back.sitecog.com/content/v1/pages/home?lang=en';
// Скрипт сборки или фоновая задача: спокойно пересидит 429
const res = await fetchWithRetry(url, { headers });
// Рендер страницы для посетителя: минуту страницу никто ждать не будет.
// 429 вернётся сразу, а вы покажете копию, сохранённую раньше.
const quick = await fetchWithRetry(url, { headers }, { retries: 1, maxWaitMs: 2_000 });Арифметика SSR: сколько запросов делает ваш сервер
Давайте посчитаем. Допустим, при рендере страница запрашивает две вещи: саму себя (/v1/pages/about) и общие шапку с подвалом (/v1/pages/common). Это два запроса на один просмотр. На сайте 30 страниц на 2 языках.
| Подход | Запросов к API в минуту | Где ломается |
|---|---|---|
| Без кэша, страница + common на каждый просмотр | 2 × просмотры | 150 просмотров в минуту — это 2,5 посетителя в секунду, хватит одной рассылки |
| Без кэша, каждый блок отдельным запросом (скажем, 20 блоков) | 20 × просмотры | 15 просмотров в минуту — оживлённый обеденный перерыв |
revalidate: 60, страница + common | не больше 30 × 2 + 1 × 2 = 62 | нигде — те же 62 и при 10 просмотрах, и при 100 000 |
Последняя строка — и есть вся суть: с кэшем на сервере каждый уникальный адрес запрашивается не чаще раза в минуту, и нагрузка на API ограничена размером сайта, а не его популярностью. Попасть в тренды перестаёт быть проблемой инфраструктуры.
Следите за сборкой
Статическая генерация — всплеск по определению: сборка, которая параллельно рендерит 400 страниц на 2 языках, за несколько секунд может выпустить с одной машины 800 запросов — намного больше 300 в минуту. Ограничьте параллельность несколькими страницами за раз и используйте терпеливый fetchWithRetry из примера выше: тогда 429 поставит сборку на паузу на минуту, а не уронит её.
Как держаться от лимитов подальше
- Кэшируйте у себя.
revalidateв Next.js, ISR или маленький кэш в памяти на любом Node-сервере. Готовые рецепты — на странице Кэш и ETag. - Один запрос страницы вместо россыпи блоков.
/v1/pages/:markerотдаёт все секции и блоки разом. Заказывать пиццу по кусочку весело только курьеру. - Общий контент — одним запросом. Шапка и подвал на странице
commonодинаковы для всех маршрутов — пусть их обслуживает один закэшированный запрос. - Пользуйтесь ETag. Запросов меньше не станет, но каждый повторный почти ничего не будет стоить по трафику. Подробнее — в разделе ETag и 304.
- Не опрашивайте API по кругу. Спрашивать каждую секунду «а не поменялось ли что?» — ровно тот трафик, ради которого лимиты и придуманы. Кэша на 60 секунд редакторам хватает, а режим Live показывает правки мгновенно.
- Следите за ключами. Старая тестовая выкладка или забытая cron-задача с отозванным ключом на том же сервере, что и боевой сайт, может за минуту исчерпать 20 запросов с неверным ключом — и тогда
429получит и боевой сайт с этого IP. Одна паршивая овца, как говорится.
curl -s -o /dev/null -w "%{http_code}\n" \
"https://back.sitecog.com/content/v1/langs" -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"
# 200 — всё хорошо, 401 — почините ключ, пока его никто не начал повторятьВсё-таки поймали 429 и хотите знать, что ещё может пойти не так? Все статусы — на странице Ошибки.