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-* теж немає — тож не шукайте їх. Оскільки вікна — це фіксовані хвилини, правило просте: зачекайте приблизно хвилину (з невеликою випадковістю, див. нижче) і спробуйте знову.
Повтор із backoff і jitter
Невелика обгортка над fetch, яка робить правильну річ для кожного типу збою:
- 429 — перечекати вікно: близько 60 секунд плюс кілька випадкових секунд.
- 5xx і мережеві помилки — експоненційний backoff із jitter: приблизно 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 // вікно — хвилина: перечекати її, плюс jitter
: 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 сторінок двома мовами.
| Підхід | Запитів до API за хвилину | Де ламається |
|---|---|---|
| Без кешу, сторінка + common на кожен перегляд | 2 × перегляди | 150 переглядів / хвилину — це 2,5 відвідувача на секунду, вистачить однієї розсилки |
| Без кешу, кожен блок окремим запитом (скажімо, 20 блоків) | 20 × перегляди | 15 переглядів / хвилину — жвава обідня перерва |
revalidate: 60, сторінка + common | не більше 30 × 2 + 1 × 2 = 62 | ніколи — ті самі 62 і для 10 переглядів, і для 100 000 |
Останній рядок — у цьому вся суть: із серверним кешем кожен унікальний URL запитується не частіше разу на хвилину, тож використання API обмежене розміром вашого сайту, а не його популярністю. Вірусний успіх перестає бути проблемою інфраструктури.
Стежте за збірками
Статична генерація — сплеск за своєю природою: збірка, що паралельно рендерить 400 сторінок двома мовами, може випустити 800 запитів за кілька секунд з однієї машини — значно більше за 300 на хвилину. Обмежте паралельність кількома сторінками водночас і використовуйте терплячий fetchWithRetry вище, щоб 429 ставив збірку на паузу на хвилину, а не валив її.
Як триматися подалі від лімітів
- Кешуйте на своєму боці.
revalidateу Next.js, ISR або невеликий кеш у пам’яті на будь-якому Node-сервері. Рецепти — на сторінці Кешування. - Один запит сторінки замість багатьох блоків.
/v1/pages/:markerповертає всі секції та блоки за раз. Замовляти піцу по шматочку весело хіба що кур’єрові. - Запитуйте спільний контент один раз. Блоки шапки й підвалу на сторінці
commonоднакові для всіх маршрутів — нехай їх усіх обслуговує один закешований запит. - Використовуйте ETag. Він не зменшує кількість запитів, але робить кожен повторний запит майже безкоштовним за трафіком. Див. ETag і 304.
- Не опитуйте API постійно. Питати API щосекунди, чи щось змінилося, — це саме той трафік, заради якого існують ліміти. 60-секундний кеш дає редакторам досить швидкий зворотний зв’язок, а режим Live — миттєвий.
- Тримайте ключі в порядку. Старе preview-розгортання чи забута cron-задача з відкликаним ключем, що працює на тому самому сервері, що й production, може спалити 20 запитів із поганим ключем за хвилину — і тоді production на цій IP теж отримує
429. Одна паршива вівця справді псує всю отару.
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 і хочете знати, що ще може піти не так? Усі статуси — на сторінці Помилки.