RateLimit-Policy: пусть ИИ-агенты сами сбавляют темп
RateLimit-Policy (черновик IETF, ревизия 11) позволяет ИИ-агентам узнать вашу квоту и сбавить темп, не дожидаясь 429. Синтаксис, nginx, Nitro, FastAPI, curl.

Заголовок 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 секунд — столько осталось до конца окна.
Параметров немного, и каждый определён в черновике (IETF draft):
| Поле | Параметр | Обязателен | Значение |
|---|---|---|---|
RateLimit-Policy | q | да | Квота, выделенная политикой, в единицах квоты |
RateLimit-Policy | w | нет | Окно в секундах, ненулевое целое |
RateLimit-Policy | qu | нет | Единица квоты; по умолчанию requests, также content-bytes и concurrent-requests |
RateLimit | r | да | Квота, ещё доступная по названной политике |
RateLimit | t | нет | Секунды, в течение которых клиент может использовать не больше 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 означает, что клиент отправил слишком много запросов за заданное время. Кэши не должны сохранять ответ 429, а сам ответ может содержать 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 и советует клиентам задать собственные пороги.
Вот этот цикл (шаги со второго по четвёртый) для 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, сканер только для чтения, который проверяет, может ли ИИ-агент прочитать и вызвать сайт. Сканер серверно рендерит собственный контент как рабочий пример.