API без лимита запросов работает прекрасно ровно до того момента, когда какой-нибудь клиент напишет цикл без единой паузы. После этого один вызывающий съедает ёмкость, которую делят все, задержка растёт для всех остальных пользователей, а сбой выглядит как отказ сервиса, а не как один некорректный скрипт. Лимит запросов — это механизм, который превращает такой сценарий в маленькую, локализованную и ясно помеченную ошибку для одного клиента.
Правильно настроить лимиты — прежде всего вопрос определённости: решить, что вы считаете, за какой период, на кого и что говорите клиенту, который вышел за рамки. В руководстве — алгоритмы, выбор обоснованных чисел, заголовки, благодаря которым хорошо написанный клиент остаётся внутри ваших лимитов, и правильное замедление, когда клиент — вы сами.
Зачем нужны лимиты запросов
Лимиты обычно вводят после инцидента, но они решают сразу четыре разные задачи:
- Защита ёмкости. У вашего сервиса есть максимальная устойчивая пропускная способность. Лимит удерживает нагрузку ниже неё по замыслу, а не по везению.
- Справедливость. Без лимита на клиента больше всего ресурсов получает самый шумный вызывающий. С лимитом каждый получает предсказуемую долю.
- Контроль расходов. Если каждый запрос вызывает обращение к базе, вызов сторонней службы или запуск модели, то API без лимита — это счёт без лимита.
- Устойчивость к злоупотреблениям. Подбор учётных данных, скрейпинг и перебор зависят от объёма. Лимит делает их достаточно медленными, чтобы они были непрактичны, и достаточно заметными, чтобы на них сработало оповещение.
Есть и пятая, менее заметная польза: описанный лимит — это договор. Клиентов можно писать так, чтобы они его соблюдали, и это куда лучше, чем узнавать свой потолок, ударившись о него.
Алгоритмы, которые стоит знать
Любой лимитер отвечает на один и тот же вопрос — разрешён ли этот запрос сейчас? — но они по-разному относятся к всплескам, и именно эту разницу вы ощущаете как клиент.
| Алгоритм | Разрешает всплески? | Лучше всего для |
|---|---|---|
| Token bucket | Да, до размера ведра | Публичных API; обычный выбор по умолчанию |
| Leaky bucket | Нет, поток на выходе идеально ровный | Защиты бэкенда с фиксированной ёмкостью |
| Фиксированное окно | Да, но несправедливо на границах | Простых квот, где точность не важна |
| Скользящее окно | Только в пределах настоящего лимита | Точного соблюдения; требует больше состояния |
Token bucket
Представьте ведро, в котором лежит фиксированное число токенов и которое пополняется с постоянной скоростью. Каждый запрос забирает один токен. Если ведро пусто, запрос отклоняется. Два числа описывают его полностью: скорость пополнения — ваша устойчивая пропускная способность, и размер ведра — самый большой всплеск, который вы готовы терпеть.
Ведро на 100 токенов, пополняемое по 10 в секунду, позволяет простаивавшему клиенту сразу отправить 100 запросов, а затем сводит его к 10 в секунду. Обычно это и нужно: настоящие клиенты работают рывками, поэтому короткий всплеск — нормальный трафик, а не злоупотребление, тогда как средняя величина на длинном отрезке остаётся ограниченной. Token bucket не случайно выбор по умолчанию: он дёшев в реализации, требует лишь счётчика токенов и метки времени на клиента, а его поведение легко объяснить в документации.
Leaky bucket
Leaky bucket — зеркальное отражение. Запросы попадают в очередь и покидают её с постоянной скоростью, как вода через отверстие. Если очередь полна, новые запросы отклоняются. Поток на выходе идеально ровный, и это именно то, что нужно перед бэкендом, который вовсе не переносит всплесков: устаревшей системой или сторонним API, который вы перепродаёте.
Цена — задержка. Запрос, попавший в почти полную очередь, ждёт, а не падает быстро, и клиент не отличит медленный сервер от запроса в очереди. Используйте его, чтобы сглаживать трафик к чему-то хрупкому; для защиты собственного эластичного сервиса лучше token bucket.
Фиксированные и скользящие окна
Фиксированное окно считает запросы за календарный период: 1000 в час, сброс в начале часа. Реализуется тривиально — счётчик и ключ, — и имеет известный изъян: всплеск на границе. Клиент, отправивший 1000 запросов в 10:59:59 и ещё 1000 в 11:00:01, остался внутри политики, но доставил 2000 запросов за две секунды — вдвое больше задуманного пика.
Скользящее окно исправляет это, считая за предыдущий период, а не за текущий календарный блок. В 11:00:01 оно всё ещё видит запросы из 10:59:59 и отклоняет излишек. Точный вариант хранит метку времени на каждый запрос, что стоит памяти; распространённый компромисс взвешивает счёт предыдущего окна по тому, какая его часть всё ещё перекрывается, — достаточно точно почти для любого API при малой доле состояния.
Как выбрать лимиты
Берите числа из измерений, а не из круглой цифры, которая выглядит щедро. Работающая последовательность:
- Измерьте свой потолок. Нагружайте, пока не начнёт деградировать задержка. Эта пропускная способность минус запас — ваша реальная ёмкость.
- Разделите на ожидаемое число одновременных клиентов. Это отправная точка для лимита на клиента, а не окончательный ответ.
- Посмотрите на текущий трафик. Найдите 95-й и 99-й процентили частоты запросов на клиента. Лимит ниже 99-го процентиля будет будить вас из-за легитимных пользователей.
- Дайте запас на всплеск. Примерно 10–20 величин темпа в секунду — распространённый размер ведра; он поглощает обычные загрузки страниц и не позволяет устойчивого злоупотребления.
- Сделайте уровни. Анонимные, аутентифицированные и платящие клиенты не должны делить один лимит. Брать плату за более высокий потолок — к тому же самый честный способ монетизировать API.
- Сначала наблюдайте, потом применяйте. Неделю логируйте, что было бы отклонено, прежде чем начать отклонять.
Ключ, по которому вы считаете, важен не меньше числа. По IP — единственный вариант для анонимного трафика, но он наказывает общие сети и NAT; по ключу API — справедливо и правильно по умолчанию для аутентифицированных вызовов; по аккаунту — не даёт клиенту купить дополнительную ёмкость, создавая ключи. Многие API применяют все три сразу: глобальный лимит, лимит на ключ и лимит на эндпоинт для дорогих маршрутов.
Когда число получено, арифметику его превращения в интервал между запросами легко слегка переврать. Калькулятор лимита запросов переводит лимит и окно в минимальную задержку между запросами, безопасный устойчивый темп и значение Retry-After, которое можно публиковать.
Заголовки, которые нужны клиентам
Лимит, которого клиент не видит, — это лимит, в который клиент упрётся. Отправляйте своё состояние в каждом ответе, а не только при отказах:
RateLimit-Limit— потолок текущего окна.RateLimit-Remaining— сколько запросов в нём осталось.RateLimit-Reset— когда квота обновится, в секундах от текущего момента.Retry-After— при429или503, сколько ждать. Либо число секунд, либо дата в формате HTTP.
Написания с X-RateLimit-* по-прежнему широко распространены и вполне работают; приведённые выше имена без префикса — стандартизованная форма, и отправлять оба набора во время перехода ничего не стоит. Важно, чтобы значения присутствовали и были верными, потому что они позволяют хорошему клиенту держать свой темп и вообще не видеть 429.
Когда вы отказываете, возвращайте 429 Too Many Requests, никогда 403 и никогда общий 500. 429 — единственный код, который сообщает клиенту, что запрос был верным и позже сработает, и только этот сигнал даёт правильное поведение при повторах. Наш справочник кодов состояния HTTP разбирает соседние коды и то, что каждый из них требует от клиента.
Как клиенту обрабатывать 429
С другой стороны соединения правила коротки и почти всегда реализованы не до конца:
- Соблюдайте
Retry-After. Если сервер говорит, сколько ждать, ждите именно столько. Ничего более осведомлённого у вас нет. - Иначе замедляйтесь экспоненциально. Ждите 1 с, 2 с, 4 с, 8 с — до предела. Немедленный повтор превращает перегрузку в давку, устроенную вами же.
- Добавьте разброс. Случайно меняйте каждую паузу на несколько сотен миллисекунд. Без этого все клиенты, упавшие вместе, повторяют вместе, и ваша схема ожидания воспроизводит тот самый пик, который должна была растянуть.
- Ограничьте число попыток. Три-пять повторов, затем сообщите об ошибке. Бесконечные повторы скрывают настоящий сбой.
- Держите темп по заголовкам. Читайте
RateLimit-Remainingи замедляйтесь до того, как получите отказ. Это разница между клиентом, который работает, и клиентом, который работает почти всегда. - Никогда не повторяйте
4xx, кроме429.400или422будут падать одинаково каждый раз, потому что проблема в самом запросе.
Типичные ошибки
- Возвращать
403вместо429. Клиенты читают403как окончательный и останавливаются либо повторяют слепо; ни одно поведение вам не подходит. - Опускать
Retry-After. Вы предложили клиенту угадывать, и обычная догадка — «сразу же». - Ограничивать только по IP. Целый офис за одним адресом NAT становится одним клиентом, а распределённый скрейпер — тысячей.
- Считать все эндпоинты одинаково. Чтение из кеша и отчёт, который считается восемь секунд, не должны стоить по одному токену; взвешивайте дорогие маршруты или давайте им отдельный лимит.
- Фиксированное окно с узким лимитом. Из-за всплеска на границе ваш настоящий пик вдвое больше опубликованного числа.
- Полное отсутствие запаса на всплеск. Идеально ровный лимит отклоняет обычный трафик, потому что настоящие клиенты приходят пачками.
- Недокументированные лимиты. Если числа нет ни в документации, ни в заголовках, каждый интегратор узнаёт его в продакшене.
- Повторы без разброса. Синхронные повторы — это то, как короткий всплеск становится затяжным.
Рассчитайте свои лимиты
Хороший лимит запросов скучен: клиенты держат темп по вашим заголовкам, всплески поглощаются, злоупотребления сдерживаются, и никто ничего не замечает. Прийти к этому — в основном арифметика и честность насчёт своей реальной ёмкости. Начните с лимита и окна, которые действительно можете держать, переведите их в интервал и значение Retry-After, нужные вашим клиентам, опубликуйте и то и другое и логируйте почти-превышения, чтобы отличать слишком узкий лимит от некорректного клиента.
Связанные инструменты и руководства:
- Калькулятор лимита запросов — переводит лимит и окно в интервалы, размеры всплеска и
Retry-After. - Коды состояния HTTP — что
429,503и остальные требуют от клиента. - Коды состояния HTTP: полное руководство — все классы ответов с разбором.
- DNS-запрос — проверьте, до какого хоста действительно доходит трафик вашего API.
Частые вопросы
Какой лимит запросов разумно взять по умолчанию для публичного API?⌄
Универсального числа нет, потому что всё зависит от того, во что вам обходится один запрос. Распространённая отправная точка для JSON API с преобладанием чтения — 1000 запросов в час на ключ при всплеске от 20 до 60, а дальше подстройка по собственным процентилям. Измерьте устойчивую пропускную способность, разделите на ожидаемое число одновременных клиентов и держите лимит выше 99-го процентиля легитимного трафика, чтобы вас не будили из-за реальных пользователей.
Чем token bucket отличается от leaky bucket?⌄
Token bucket разрешает всплески до размера ведра и затем сводит темп к скорости пополнения, что подходит публичным API, поскольку настоящие клиенты приходят пачками. Leaky bucket ставит запросы в очередь и выпускает их с постоянной скоростью, поэтому поток на выходе идеально ровный, но запросы ждут, а не падают быстро. Leaky bucket ставьте перед бэкендом, который не переносит всплесков, token bucket — во всех остальных случаях.
Почему фиксированное окно — это проблема?⌄
Из-за всплеска на границе. Клиент, израсходовавший всю квоту в самом конце одного окна и всю квоту в начале следующего, доставляет за пару секунд вдвое больше задуманного пика, формально оставаясь внутри политики. Скользящее окно считает за предыдущий период, поэтому такая схема корректно отклоняется.
Какой код состояния должен возвращать лимитер?⌄
429 Too Many Requests с заголовком Retry-After. Это единственный код, который сообщает клиенту, что запрос был верным и позже сработает, и именно он даёт правильное поведение при повторах. 403 читается как окончательный, а 500 — как ошибка сервера, так что оба подталкивают клиентов к неправильным действиям.
Как моему клиенту обрабатывать 429?⌄
Соблюдайте Retry-After, если он есть. Если его нет, замедляйтесь экспоненциально — 1 с, 2 с, 4 с, 8 с — добавляя к каждой паузе случайный разброс, чтобы клиенты, упавшие вместе, не повторяли вместе. Ограничьте число попыток тремя-пятью и затем сообщите об ошибке. Ещё лучше — читайте RateLimit-Remaining в каждом ответе и замедляйтесь до отказа.
Ограничивать по IP-адресу или по ключу API?⌄
По ключу API для аутентифицированного трафика: это справедливо, переживает смену сети клиентом и позволяет вводить уровни. IP — единственный вариант для анонимных запросов, но он считает целый офис за одним NAT одним клиентом, а распределённый скрейпер — многими. Большинство зрелых API применяют и то и другое, плюс более строгий лимит на эндпоинт для дорогих маршрутов.
Более 15 лет практического опыта в веб-разработке и безопасности. Специализация — практическая веб-безопасность: чтобы сайты были защищёнными, надёжными и легко диагностировались. BuildToolForge создан, чтобы такие проверки были бесплатными, приватными и понятными. Инструменты по возможности работают прямо в браузере, а всё, что вы вводите в инструмент, не сохраняется.
Как мы пишем руководства: Каждое руководство основано на практическом опыте, сверено с официальными стандартами и проверено нашими инструментами на реальных доменах. Мы обновляем руководства, когда меняются стандарты или инструменты. Нашли ошибку? Напишите нам: contact@buildtoolforge.com.