Перейти до основного вмісту
БЛОГ

RateLimit-Policy: нехай ШІ-агенти самі пригальмовують

RateLimit-Policy (чернетка IETF, ревізія 11) дає ШІ-агентам змогу дізнатися вашу квоту й пригальмувати, не чекаючи 429. Синтаксис, nginx, Nitro, FastAPI, curl.

Автор: IMozzОновлено 2026-10-29
Ліміти запитів для ШІ-агентів: RateLimit-Policy, RateLimit, Retry-After — aiSiteReady

Заголовок RateLimit-Policy повідомляє ШІ-агентові, скільки запитів ваш сервіс приймає за часове вікно, а парний до нього RateLimit — скільки з них лишилося саме зараз. Агент, який читає обидва, сам тримає темп, пригальмовує до того, як квота закінчиться, і чекає рівно стільки секунд, скільки треба, замість того щоб намацувати ваш ліміт через 429 Too Many Requests чи блокування за IP.

Ліміти запитів належать до шару Правила для ботів у стеку готовності. robots.txt каже, чи може краулер заходити; ці заголовки — як швидко йому можна рухатися. Найважливіші вони для API, який ви відкриваєте агентам (див. як зробити сайт доступним для виклику агентами та каталог API), але ті самі два рядки на головній сторінці вже підказують краулеру, чого чекати.

Обидва поля описані в IETF Internet-Draft, а не в RFC, тому я позначаю те, що ще може змінитися. Загальну картину описано в матеріалі про те, що означає готовність до ШІ-агентів.

Ключові висновки

  • Два поля відповіді, обидва — списки елементів Structured Field: RateLimit-Policy (q — квота, w — вікно, необов'язкові qu — одиниця виміру й pk — ключ розділу) та RateLimit (r — залишок, t — ефективне вікно в секундах) (IETF draft).
  • Статус на жовтень 2026: Internet-Draft, ревізія 11 від 23 травня 2026, номера RFC немає; синтаксис востаннє змінювався в ревізії 08, але відкриті pull request перейменували б параметри r і t поля RateLimit на a і w, дозволили б назви політик без лапок і додали б поле RateLimit-Partition. Вважайте приклади поточною формою, а не обіцянкою.
  • Клієнта зупиняє саме 429 із Retry-After (RFC 6585, RFC 9110); за чернеткою за наявності обох пріоритет у Retry-After.
  • Оголошуйте постійну політику на сторінках, а живий залишок — у некешованих відповідях API, і лише для лімітів, які справді застосовуєте. Станом на жовтень 2026 року фронтенд нашого сайту жорстко прописує q=20;w=60, а бекенд бере ліміт із конфігу — ризик розсинхронізації описано нижче.
  • Перевірка rate_limit_hints в aiSiteReady зараховує PASS за придатний до розбору RateLimit чи RateLimit-Policy або коректний Retry-After; застарілі X-RateLimit-* і самотній 429 дають лише PARTIAL (подробиці наприкінці).

Що насправді кажуть RateLimit-Policy і RateLimit?

Це два поля відповіді, що ділять одне завдання навпіл: RateLimit-Policy описує постійне правило, а RateLimit повідомляє поточний стан за цим правилом. Обидва — списки Structured Field (RFC 9651), елементи яких — рядки в лапках із назвою політики, за якими через ; ідуть параметри. Саме назва політики пов'язує два заголовки.

Ось робочий приклад із узгодженими числами. Політика "api";q=100;w=60 дозволяє 100 запитів за вікно в 60 секунд. На 25-й секунді вікна агент витратив 60 із них, тому сервер відповідає "api";r=40;t=35: не більше ніж 40 запитів за наступні 35 секунд — стільки лишилося до кінця вікна.

Два заголовки: RateLimit-Policy (api, q=100, w=60) — постійне правило; RateLimit (api, r=40, t=35) — поточний стан: використано 60 зі 100, вікно 35 с.

Параметрів небагато, і кожен визначено в чернетці (IETF draft):

ПолеПараметрОбов'язковийЗначення
RateLimit-PolicyqтакКвота, виділена політикою, в одиницях квоти
RateLimit-PolicywніВікно в секундах, ненульове ціле
RateLimit-PolicyquніОдиниця квоти; за замовчуванням requests, також content-bytes і concurrent-requests
RateLimitrтакКвота, що ще доступна за названою політикою
RateLimittніСекунди, протягом яких клієнт може використати не більше ніж r
обидваpkніКлюч розділу (byte sequence) для квот на користувача чи ресурс

Три правила чернетки визначають реалізацію. RateLimit-Policy має лишатися незмінним від відповіді до відповіді, тоді як RateLimit може змінюватися в кожній. Клієнти мають право ігнорувати деталі політики, зобов'язані ігнорувати некоректні поля й не повинні вважати, що наступні відповіді взагалі міститимуть ці заголовки. А додатний r — підказка, а не гарантія: клієнти не повинні сприймати його як угоду про рівень сервісу. У списку може бути кілька політик, наприклад "burst";q=100;w=60,"daily";q=1000;w=86400.

Це вже стандарт, і які заголовки віддавати?

Поки що ні: це Internet-Draft IETF. Віддавайте пару з ревізії 08 — RateLimit-Policy і RateLimit — плюс Retry-After у кожному 429. Синтаксис змінювався вже кілька разів, востаннє — в ревізії 08. Станом на жовтень 2026 остання ревізія — draft-ietf-httpapi-ratelimit-headers-11 від 23 травня 2026. Вона належить робочій групі IETF HTTPAPI, у шапці вказано Standards Track, а datatracker не показує номера RFC і повідомляє термін дії до 24 листопада 2026 (datatracker).

Підтримка нерівномірна, і про це варто сказати один раз прямо. REST API GitHub віддає заголовки x-ratelimit-*, де скидання — це мітка UTC epoch (документація GitHub). Фільтр rate limit в Envoy вміє віддавати X-RateLimit-* за чернеткою draft-polli-03, а не під актуальними назвами (документація Envoy). У FAQ краулера Anthropic сказано, що його боти враховують Crawl-delay із robots.txt «де доречно» (Anthropic).

Сигнал, який розуміє кожен клієнт, — це 429 із Retry-After; Google, наприклад, вважає 429 ознакою перевантаження сервера й уповільнює обхід (Google). Нові заголовки все одно варті двох рядків: вони майже нічого не коштують, а все, що їх читає, зокрема сканери на кшталт aiSiteReady, бачить ваші ліміти.

Історія чернетки пояснює, чому старі посібники суперечать один одному. Щоб скласти цю таблицю, я порівняв архівні тексти ревізій:

Ревізія (дата)Як виглядали заголовки
Вендорна конвенція, не стандартизованаX-RateLimit-Limit, -Remaining, -Reset; скидання може бути в секундах, мілісекундах, epoch або датою
00–03 (груд. 2020 — берез. 2022)RateLimit-Limit: 100, RateLimit-Remaining: 50, RateLimit-Reset: 7
04–06 (трав. — груд. 2022)Ті самі три плюс RateLimit-Policy: 100;w=10
07 (черв. 2023)Поле-словник RateLimit: limit=100, remaining=50, reset=5 поряд із RateLimit-Policy: 100;w=10
08–11 (жовт. 2024 — трав. 2026)Списки елементів: RateLimit: "api";r=40;t=35 і RateLimit-Policy: "api";q=100;w=60

Що віддавати? Пару з ревізій 08 і новіших: це актуальна форма й єдина з іменованими політиками. Застарілі X-RateLimit-* залишайте, лише якщо від них залежить відомий вам клієнт, і з тими самими числами. Трійку RateLimit-Limit/-Remaining/-Reset з ревізій 00–06 і словниковий синтаксис ревізії 07 не додавайте.

Ендпоінт сканування aiSiteReady (POST /api/v1/scans) при 429 віддає обидві родини: Retry-After, трійку X-RateLimit-* і сучасну пару. Це перестраховка, поки чернетка не усталилася, а не взірець на віки.

Яке місце посідають 429 і Retry-After?

Вони — виконавча половина; нові заголовки — прогноз. 429 Too Many Requests означає, що клієнт надіслав забагато запитів за певний час. Відповідь може містити Retry-After, а кеші не повинні її зберігати (RFC 6585, розділ 4). Retry-After містить або кількість секунд, або HTTP-дату (RFC 9110, розділ 10.2.3).

Чернетка не прив'язана до коду статусу: RateLimit може з'явитися в будь-якій відповіді, а власний FAQ чернетки каже, що специфікація не пов'язана з 429. Якщо у відповіді є і RateLimit, і Retry-After, клієнт зобов'язаний дотримуватися Retry-After, а сервер не повинен ставити його раніше за кінець ефективного вікна. Повна відповідь з обмеженням виглядає так:

HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 35
RateLimit-Policy: "api";q=100;w=60
RateLimit: "api";r=0;t=35

{
  "type": "https://iana.org/assignments/http-problem-types#quota-exceeded",
  "title": "Too Many Requests",
  "status": 429,
  "violated-policies": ["api"]
}

URI в type — це тип проблеми, який чернетка пропонує зареєструвати на основі RFC 9457; звичайне JSON-тіло помилки теж підійде. Основну роботу роблять заголовки, а Retry-After тут навмисно дорівнює t, щоб два сигнали ніколи не суперечили один одному.

Як ШІ-агентові планувати запити й пригальмовувати?

Агентові треба тримати темп за політикою, робити паузу, коли r близький до нуля, дотримуватися Retry-After при 429 й обмежувати будь-яке очікування. По-перше, один раз прочитати RateLimit-Policy і триматися темпу: для "api";q=100;w=60 це один запит раз на 0,6 секунди (60 поділити на 100), і квоту при цьому не буде перевищено.

По-друге, читати RateLimit після кожної відповіді й, коли r близький до нуля, притримати наступний запит на t секунд. Чернетка допускає обидва стилі: сповільнюватися в міру наближення до ліміту або витратити всю квоту й потім чекати.

По-третє, при 429 чи 503 чекати Retry-After; якщо його немає — t секунд із RateLimit, а далі експоненційна затримка з джитером. По-четверте, обмежувати, скільки ви готові чекати, й ігнорувати сміття: розділ безпеки чернетки застерігає від значень на кшталт Retry-After: 1000000 і радить клієнтам задати власні пороги.

Як агент пригальмовує: 200 з r=1, t=35 — пауза 35 с; 429 з Retry-After: 35 — чекає 35 с плюс джитер і повторює запит.

Ось цей цикл (кроки з другого до четвертого) для Node 18 і новіших із глобальним fetch. У жовтні 2026 року я запускав його на middleware FastAPI з прикладу нижче з малою квотою — 4 запити за 8 секунд: він робив паузи між вікнами й жодного разу не отримав 429, а коли я спершу вичерпав квоту через curl, дочекався Retry-After.

const sleep = (s: number) => new Promise((r) => setTimeout(r, s * 1000));

// Мінімальний розбір типової форми "api";r=40;t=35. Для всього екзотичнішого
// (ключі розділу тощо) беріть бібліотеку Structured Fields з RFC 9651.
function readRateLimit(value: string | null) {
  if (!value) return null;
  const items = value.split(/,(?=\s*")/).flatMap((member) => {
    const [name, ...params] = member.trim().split(";");
    const p = Object.fromEntries(params.map((s) => s.trim().split("=")));
    const r = Number(p.r);
    const t = p.t === undefined ? undefined : Number(p.t); // ефективне вікно, секунди
    if (!/^".+"$/.test(name) || !Number.isInteger(r) || r < 0) return []; // некоректне: ігноруємо
    if (t !== undefined && (!Number.isInteger(t) || t < 0)) return []; // те саме для t
    return [{ policy: name.slice(1, -1), remaining: r, reset: t }];
  });
  // Якщо політик кілька, орієнтуємося на ту, що найближча до вичерпання.
  return items.sort((a, b) => a.remaining - b.remaining)[0] ?? null;
}

// Retry-After — це delay-seconds або HTTP-дата (RFC 9110, розділ 10.2.3).
function retryAfterSeconds(value: string | null): number | null {
  if (!value) return null;
  if (/^\d+$/.test(value.trim())) return Number(value);
  const at = /[a-z]/i.test(value) ? Date.parse(value) : NaN; // в HTTP-даті є літери
  return Number.isNaN(at) ? null : Math.max(0, Math.ceil((at - Date.now()) / 1000));
}

const MAX_WAIT = 600; // секунд; абсурдні очікування не чекаємо, а повідомляємо про них
let notBefore = 0; // epoch у мс: наступний запит не раніше цього моменту

export async function politeFetch(url: string, attempts = 4): Promise<Response> {
  for (let i = 0; ; i++) {
    const pause = notBefore - Date.now();
    if (pause > 0) await sleep(pause / 1000);

    const res = await fetch(url, {
      headers: { "User-Agent": "acme-agent/1.0 (+https://acme.example/bot)" },
    });
    const limit = readRateLimit(res.headers.get("ratelimit"));

    if (res.status === 429 || res.status === 503) {
      // Retry-After пріоритетніший за RateLimit, якщо є обидва.
      const wait = retryAfterSeconds(res.headers.get("retry-after")) ?? limit?.reset ?? 2 ** i;
      if (i + 1 >= attempts || wait > MAX_WAIT) return res; // здаємося, нехай код, що викликав функцію, побачить відповідь
      notBefore = Date.now() + (wait + Math.random()) * 1000; // джитер
      await res.body?.cancel(); // відкидаємо тіло перед повтором
      continue;
    }

    // Квота майже скінчилася: притримуємо наступний запит на t секунд (ефективне вікно).
    if (limit && limit.remaining <= 1 && limit.reset !== undefined && limit.reset <= MAX_WAIT) {
      notBefore = Date.now() + limit.reset * 1000;
    }
    return res;
  }
}

Цикл мінімальний: один origin, один процес. Справжньому агентові потрібен бюджет на кожен origin, спільний для всіх воркерів, та інформативний User-Agent.

Як віддавати заголовки з nginx, Nitro і FastAPI?

Публікуйте постійну політику скрізь, куди агент потрапляє першим, а живий залишок — лише там, де запити рахує лімітер. Статичний RateLimit-Policy підходить для кешованих сторінок, бо не змінюється, а живий RateLimit — ні: чернетка велить клієнтам ігнорувати обидва поля у відповідях із кешу (IETF draft, розділ 7.3). Втратити статичну політику при влучанні в кеш майже не страшно, а закешований живий r був би хибним.

aiSiteReady ділить їх саме так: HTML-сторінки несуть "scan";q=20;w=60, а ендпоінт сканування додає живу пару. scan — наша мітка для API сканування; чернетка не надає назвам політик жодного значення, окрім зв'язку двох заголовків, тож краулер може застосувати q=20;w=60 і до самих сторінок — такий темп нас влаштовує.

nginx уміє застосовувати ліміт і оголошувати політику, але не може повідомити залишок квоти. Модуль limit_req працює за схемою «діряве відро» й дає лише змінну $limit_req_status, тож ви віддаєте статичний RateLimit-Policy і Retry-After при відмовах (nginx limit_req).

# контекст http {}
limit_req_zone $binary_remote_addr zone=api:10m rate=60r/m;
limit_req_status 429;                 # статус відмови за замовчуванням — 503

map $status $retry_after {
    429     1;                        # один запит на секунду: слот звільниться за 1 с
    default "";                       # порожнє значення — заголовок не надсилається
}

server {
    listen 443 ssl;
    server_name api.example.com;

    location /api/ {
        limit_req zone=api burst=59 nodelay;   # 60 одразу, далі один на секунду

        add_header RateLimit-Policy '"api";q=60;w=60' always;
        add_header Retry-After $retry_after always;

        proxy_pass http://app_backend;
    }
}

Важливі три деталі. Без always директива add_header застосовується лише до фіксованого списку кодів успіху й редиректів, тож ваш 429 нічого не отримає. За замовчуванням директиви успадковуються, лише якщо на поточному рівні немає власних add_header (у nginx 1.29.3 і новіших їх можна об'єднати директивою add_header_inherit merge;), тому один зайвий рядок у location відкидає всі вищі (модуль headers у nginx). А трюк із map працює, бо nginx не надсилає заголовок із порожнім значенням, як показує вихідний код.

Спершу спробуйте limit_req_dry_run on; і логуйте $limit_req_status, щоб побачити, що було б відхилено. Я перевірив цю конфігурацію в nginx 1.31.1 (Docker): відповіді 200 і 404 несли RateLimit-Policy, відхилені запити отримували 429 із Retry-After: 1, а з burst=59 клієнт міг надіслати 60 запитів одразу до першого 429. Якщо burst менший, оголошуйте меншу квоту, наприклад q=10;w=10 для burst=10. Рядку listen 443 ssl потрібні ще ваші директиви ssl_certificate, а proxy_pass — блок upstream app_backend.

Nitro (Nuxt 4) — спосіб, у який aiSiteReady оголошує політику на власних сторінках. Це скорочена копія плагіна, що працює на цьому сайті; у справжньому плагіні значення будується в чистій функції, яку можна тестувати у Vitest.

// server/plugins/rate-limit-policy.ts (Nitro 2)
export default defineNitroPlugin((nitroApp) => {
  const policyHeader = `"scan";q=20;w=60`;

  nitroApp.hooks.hook("beforeResponse", (event) => {
    if (event.node.res.headersSent) return; // тіло вже надіслано потоком
    const contentType = getResponseHeader(event, "content-type");
    if (typeof contentType !== "string" || !contentType.includes("text/html")) return;
    setResponseHeader(event, "RateLimit-Policy", policyHeader);
  });
});

Фільтр за text/html зроблено навмисно: у шрифту чи JavaScript-чанка немає ліміту, який варто було б оголошувати. Ці назви стосуються Nitro 2, на якому працює наш сайт; у поточній документації Nitro замість них definePlugin і хук response, тож перевірте свою версію (Nitro 2, Nitro 3).

FastAPI — місце для живої пари. Цей middleware веде фіксоване вікно для кожного клієнта й віддає обидва заголовки в кожній відповіді, зокрема в 429. У жовтні 2026 року я запускав його через uvicorn і перевіряв командами curl, наведеними нижче.

import math
import time

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()

POLICY, QUOTA, WINDOW = "api", 100, 60  # має збігатися з тим, що реально застосовує лімітер нижче
_windows: dict[str, tuple[float, int]] = {}  # клієнт -> (початок вікна, лічильник)


def hit(client: str) -> tuple[bool, int, int]:
    """Рахує один запит; повертає (дозволено, залишок, секунд до скидання)."""
    now = time.monotonic()
    start, count = _windows.get(client, (now, 0))
    if now - start >= WINDOW:  # вікно скінчилося: починаємо нове
        start, count = now, 0
    count += 1
    _windows[client] = (start, count)
    reset = max(1, math.ceil(start + WINDOW - now))
    return count <= QUOTA, max(0, QUOTA - count), reset


@app.middleware("http")
async def rate_limit(request: Request, call_next):
    client = request.client.host if request.client else "unknown"
    allowed, remaining, reset = hit(client)
    headers = {
        "RateLimit-Policy": f'"{POLICY}";q={QUOTA};w={WINDOW}',
        "RateLimit": f'"{POLICY}";r={remaining};t={reset}',
    }
    if not allowed:
        headers["Retry-After"] = str(reset)
        return JSONResponse({"detail": "Too many requests"}, status_code=429, headers=headers)
    response = await call_next(request)
    response.headers.update(headers)
    return response

Форма @app.middleware("http") узята з документації FastAPI (FastAPI). У проді замініть словник спільним сховищем; лімітер aiSiteReady використовує атомарні Redis INCR і EXPIRE, щоб усі репліки API рахували те саме вікно. За зворотним проксі ключем має бути справжня адреса клієнта, а не проксі. З бекенда aiSiteReady варто перейняти головне: обидва заголовки будуються з рішення лімітера, а не з констант:

headers = {
    "RateLimit-Policy": f'"scan";q={rule.limit};w={rule.window_seconds}',
    "RateLimit": f'"scan";r={decision.remaining};t={decision.retry_after_seconds}',
}

Які помилки ламають заголовки лімітів?

Найчастіше ламається число, що перестало відповідати реальності, або заголовок, якого немає у відповіді, прочитаній агентом.

ПомилкаЧим шкодитьЯк виправити
Оголошено політику, яку ви не застосовуєтеАгенти довіряють числам, яких сервер не дотримується, або ви блокуєте нижче за обіцяну квотуГенеруйте значення з конфігурації лімітера; оголошуйте не більше за те, що застосовуєте
Неузгодженість вікнаw=60 для «дірявого відра» чи ковзного вікна або t, порахований за іншим вікномВиводьте t із реального часу скидання лімітера; якщо вікно не фіксоване, оголошуйте із запасом
Дві копії чиселСторінка каже одну квоту, API — іншуОдне джерело істини або тест, що їх порівнює
Застарілі й сучасні заголовки розходятьсяX-RateLimit-Reset як epoch поряд із t у секундах або шлюз і застосунок додають різні набориЗаголовки веде один шар; якщо лишаєте обидва набори — з однаковими числами
Немає заголовків у відповідях із помилкоюadd_header без always пропускає 429, а 429 без Retry-After не дає інструкціїВикористовуйте always і перевіряйте шлях 429
Живий RateLimit у кешованих відповідяхКешована копія показує застарілий r; клієнтам велено її ігнорувати, а 429 кешувати не можнаСтатична політика на кешованих сторінках, живі значення — у некешованих відповідях API
Невидимі для браузерних агентівКрос-доменні скрипти не читають заголовки поза списком CORSДодайте заголовки до Access-Control-Expose-Headers

Станом на 2 жовтня 2026 року останній рядок — прогалина в API самого aiSiteReady. За замовчуванням браузери показують крос-доменним скриптам лише сім заголовків відповіді, і ні RateLimit, ні Retry-After серед них немає (MDN). CORS-конфігурація бекенда відкриває лише Content-Disposition, тож скрипт з іншого origin їх не побачить. Серверному агентові це не заважає, браузерному — завадило б.

Помилка, з якою я живу, — друга копія чисел. Коли я додав заголовок у червні 2026 року, політика для наших HTML-сторінок перетворилася на дві константи в спільному модулі фронтенда:

/** Quota (`q`): max scan requests per window. Mirrors `rate_limit_scan_ip_requests`. */
export const SCAN_RATE_LIMIT_QUOTA = 20;
/** Window (`w`) in seconds. Mirrors `rate_limit_scan_window_seconds`. */
export const SCAN_RATE_LIMIT_WINDOW_SECONDS = 60;

Бекенд же читає rate_limit_scan_ip_requests і rate_limit_scan_window_seconds зі своїх налаштувань: за замовчуванням 20 і 60, і оператор може перевизначити їх через змінні середовища. Коментар над константами вимагає тримати їх синхронними. Тест фронтенда звіряє заголовок із його ж константами, а тести бекенда перевіряють вивід "scan";q=20;w=60 для лімітів, заданих як 20 і 60, але тесту, який порівнював би одну сторону з іншою, я не знайшов.

Підніміть ліміт бекенда до 60 — і ендпоінт сканування оголошуватиме q=60, поки головна все ще каже q=20. Опустіть до 10 — і головна пообіцяє більше, ніж дає API. Лікується це єдиним джерелом істини або тестом, що перевіряє обидві сторони; станом на 2 жовтня 2026 року це коментар і два однакові значення за замовчуванням.

Як перевірити заголовки через curl?

Запросіть сторінку чи ендпоінт, до яких агент звернеться першими, прочитайте заголовки зі справжнього GET, а тоді перевищте квоту свого API. Перевіряйте публічний edge-URL: шлюзи й CDN перед вашим origin можуть додавати або прибирати заголовки. Беріть -D - -o /dev/null, а не curl -I: запит HEAD може повернути 405 або пройти повз ваш middleware.

# 1. Що агент бачить на першій завантаженій сторінці?
curl -sS -D - -o /dev/null https://example.com/ | grep -i '^ratelimit'

# 2. Чи зменшується живий залишок в API?
for i in 1 2 3; do
  curl -sS -D - -o /dev/null https://api.example.com/items/1 | grep -i '^ratelimit:'
done

# 3. Вийти за квоту (лише на своєму staging-API) і прочитати 429
for i in $(seq 1 120); do
  curl -sS -o /dev/null -w '%{http_code}\n' https://staging.example.com/items/1
done | sort | uniq -c
curl -sS -D - -o /dev/null https://staging.example.com/items/1 | grep -iE '^(HTTP|retry-after|ratelimit)'

У жовтні 2026 року на middleware FastAPI вище друга команда виводила лічильник, що зменшувався на одиницю щоразу, а третя (запущена, коли вікно вже скинулося) — 100 200 і 20 429, а далі заголовки відповіді 429. Ваші числа будуть іншими: t округлюється вгору й зменшується в міру старіння вікна.

ratelimit: "api";r=97;t=60
ratelimit: "api";r=96;t=60
ratelimit: "api";r=95;t=60

HTTP/1.1 429 Too Many Requests
ratelimit-policy: "api";q=100;w=60
ratelimit: "api";r=0;t=60
retry-after: 60

Назви заголовків не залежать від регістру, тож ratelimit: і RateLimit: — той самий заголовок. Дешеву частину варто винести в CI. Цей скрипт перевіряє лише RateLimit-Policy — одну з форм, які приймає aiSiteReady, — і лише в найпоширенішому вигляді: назва політики в лапках, одразу за нею ціле q. Сканер м'якший: проходять і будь-який порядок параметрів, і придатний до розбору RateLimit, і коректний Retry-After.

#!/usr/bin/env bash
set -euo pipefail
URL="${1:?usage: check-ratelimit.sh https://example.com/}"
HEADERS="$(curl -sS -D - -o /dev/null "$URL" | tr -d '\r')"
grep -qiE '^ratelimit-policy: "[^"]+";q=[0-9]+' <<<"$HEADERS" || { echo 'FAIL: no parseable RateLimit-Policy'; exit 1; }
grep -qiE '^x-ratelimit-' <<<"$HEADERS" && echo 'WARN: legacy X-RateLimit-* present, keep the numbers identical'
echo OK

Як aiSiteReady перевіряє заголовки лімітів?

Перевірка rate_limit_hints в aiSiteReady читає заголовки одного URL, який ви скануєте (зазвичай це головна сторінка), і оцінює знайдене. Вона належить до категорії Правила для ботів поряд із перевіркою AI bot rules (вона читає ваш robots.txt) і перевіркою правил використання контенту. Сервер вона береже: перевикористовує відповідь, яку скан уже отримав, зайвих запитів не шле й вдається до єдиного запиту HEAD, лише коли цієї відповіді немає.

Це перевірка низької серйозності, і вона виконується в усіх готових профілях сканування, від Усі перевірки до Контентний сайт і API/застосунок. Якщо API працює на іншому URL, проскануйте і його: скан головної бачить лише політику цієї сторінки.

Як вона оцінює знайдене — за документацією та кодом перевірки:

РезультатЩо побачив сканер
PASSПридатний до розбору RateLimit чи RateLimit-Policy (назва політики в лапках плюс ціле r або q) або коректний Retry-After
PARTIALЛише 429, лише застарілі заголовки (родина RateLimit-Limit, X-RateLimit-*), вендорний заголовок bot-policy або сучасний заголовок, що не розбирається
FAILНічого з переліченого
UNKNOWNЦіль відповіла антибот-блокуванням (HTTP 403) або резервний HEAD завершився тайм-аутом чи помилкою, тож надійних даних немає

Виняток для 429 зроблено навмисно. Більшість перевірок вважають і 403, і 429 ознакою «ціль нас заблокувала», але тут сам 429 — сигнал ліміту, тож 429 із коректним Retry-After усе одно проходить.

На сторінці методології наведено ваги категорій і множники серйозності. Ця перевірка має низьку серйозність (×0,5) і ділить категорію Правила для ботів (на сторінці методології — «Керування ботами») із перевірками AI bot rules і правил використання контенту, тож FAIL коштує близько 3 балів зі 100 (3,6 у профілі Контентний сайт), а PARTIAL — приблизно вдвічі менше. Перевірка добре поєднується з керуванням ШІ-краулерами через robots.txt, яке вирішує, кому взагалі можна обходити сайт; а для загальної картини пройдіть 30-хвилинний аудит готовності до ШІ-агентів.

Запустіть безкоштовне сканування, щоб побачити, чи оголошує сторінка, яку ви скануєте, придатний до розбору RateLimit-Policy, які застарілі заголовки вона досі віддає і яке виправлення зробити першим — англійською, українською чи російською.

Якщо коротко: віддавайте RateLimit-Policy для постійного правила, RateLimit для живого залишку й Retry-After у кожному 429, генеруйте всі три з лімітера, який реально їх застосовує, і перевіряйте шлях 429 через curl. Чернетка ще може змінитися, але ці рядки дозволять агентові пригальмувати самому, не чекаючи, поки це доведеться робити вам.

IMozz має 20 років у розробці ПЗ, а останній рік будує продукти за допомогою LLM. Він розвиває aiSiteReady, сканер лише для читання, що перевіряє, чи може ШІ-агент прочитати й викликати сайт. Сканер серверно рендерить власний контент як робочий приклад.

Часті запитання

Що таке заголовок RateLimit-Policy?
RateLimit-Policy — заголовок відповіді з чернетки робочої групи IETF HTTPAPI, що оголошує постійну квоту: назва політики в лапках, q — квота й необов'язковий w — вікно в секундах, наприклад "api";q=100;w=60. Значення має лишатися однаковим у всіх відповідях. Парний заголовок RateLimit несе поточний залишок (r) і ефективне вікно (t) — кількість секунд, протягом яких клієнт може витратити не більше ніж r. Поки що це Internet-Draft, а не RFC.
Що використовувати: RateLimit чи X-RateLimit?
Для нового коду віддавайте RateLimit і RateLimit-Policy з чернетки. X-RateLimit-* залиште, лише якщо від них залежить відомий вам клієнт, і стежте, щоб два набори не суперечили один одному. Заголовки з X- ніколи не стандартизували: у GitHub скидання — це мітка часу Unix (epoch, UTC), а t у чернетці — кількість секунд від поточного моменту. У будь-якому разі 429 має нести Retry-After.
Що має робити ШІ-агент, отримавши 429?
Припинити слати запити на цей origin і почекати. Якщо є Retry-After, дотримуйтеся його: це кількість секунд або HTTP-дата, і за чернеткою він пріоритетніший за RateLimit. Без нього чекайте t секунд із RateLimit, далі нарощуйте паузу експоненційно з джитером. Обмежте очікування, здавайтеся після кількох спроб і повідомляйте про помилку, а не повторюйте безкінечно.
RateLimit уже став RFC?
Ні. Станом на жовтень 2026 остання ревізія — draft-ietf-httpapi-ratelimit-headers-11 від 23 травня 2026: Internet-Draft робочої групи IETF HTTPAPI, термін дії якого спливає 24 листопада 2026, якщо його не продовжать. Синтаксис змінювався вже кілька разів, востаннє — між ревізіями 07 і 08, тож перед копіюванням прикладів із будь-якої статті, зокрема цієї, зазирніть у datatracker.