Когда API живёт за HAProxy, один из первых вопросов — как ограничить шумный трафик и не положить бэкенды. Особенно когда появляются агрессивные боты, кривые интеграции, а иногда и DDoS-подобные атаки. Встроенные stick-tables в HAProxy дают очень мощный и при этом лёгкий способ реализовать гибкий rate limit без внешних сервисов.
В этом разборе посмотрим, как строить rate limit для HTTP API на базе stick-tables: от простых лимитов по IP до более сложных сценариев — с учётом пути, ключа API, юзер-агента и статуса ответа. Параллельно обратим внимание на типичные грабли и отладку.
Зачем вообще делать rate limit на уровне HAProxy
Ограничивать запросы можно на уровне приложения, WAF, API-gateway, но HAProxy остаётся очень выгодной точкой для rate limit:
- Ранний отсев мусора — плохие запросы режутся до попадания в приложение.
- Глобальное хранилище —
stick-tablesвидят весь трафик через инстанс, а не только отдельный под или воркер. - Простая логика — декларативные ACL без Lua и внешних сервисов.
- Минимальный оверхед — счётчики в памяти без диска и сетевых запросов.
Типичные задачи, которые удобно решать rate limit на HAProxy:
- Защита API от DDoS-подобной нагрузки по IP или подсетям.
- Ограничение брутфорса логина по IP или учётной записи.
- Срезание «шумных» клиентов (битые интеграции, баги в клиентах).
- Разные лимиты для публичного и приватного API.
Если у вас API фронтируется через балансировщик на базе VDS с HAProxy, вынесенный rate limit на уровень прокси позволяет защитить сразу несколько приложений без доработок кода.
Базовая теория stick-tables в HAProxy
Stick-tables — это кэши в памяти, которые хранят ключи (например, IP клиента) и набор полей store: счётчики, скользящие окна, метки времени и т. д.
Минимальная структура может выглядеть так:
backend api_ratelimit
stick-table type ip size 1m expire 10m store http_req_rate(10s)
Расшифровка параметров:
type ip— ключом является IP клиента.size 1m— максимум 1 миллион записей (подбирается по нагрузке и памяти).expire 10m— запись живёт 10 минут после последнего доступа.store http_req_rate(10s)— храним скорость запросов за последние 10 секунд.
Дальше в фронтенде или бэкенде мы «приклеиваем» трафик к таблице с помощью stick on или track-sc и читаем значения в ACL.

Простой rate limit API по IP
Начнём с самого частого кейса: ограничить количество HTTP-запросов к API с одного IP-адреса, допустим, до 20 RPS.
Минимальный пример конфигурации
frontend api_frontend
bind :80
acl is_api path_beg /api/
use_backend api_backend if is_api
backend api_backend
stick-table type ip size 100k expire 10m store http_req_rate(10s)
# Учитываем каждый запрос по IP-адресу клиента
http-request track-sc0 src
# Порог: больше 200 запросов за 10 секунд (20 RPS)
acl too_many_requests sc0_http_req_rate gt 200
# Отбиваем лишние запросы до обращения к приложению
http-request deny status 429 content-type application/json if too_many_requests
default-server init-addr last,libc
server api1 10.0.0.10:8080 check
server api2 10.0.0.11:8080 check
Ключевые моменты:
stick-tableописана в бэкенде, где мы считаем запросы.http-request track-sc0 src— клиентский IP попадает вstick-table(счётчик обновляется на каждый запрос).sc0_http_req_rate— обращение к полюhttp_req_rate(10s)в нулевом stick-counter.gt 200— лимит: больше 200 запросов за 10 секунд (20 RPS).http-request denyсstatus 429— сразу возвращаем ошибку клиенту.
Важно: порядок правил
Порядок директив http-request имеет значение. Сначала должны идти track-sc, потом ACL, потом deny, set-header и прочее. Иначе вы будете смотреть в неинициализированный счётчик и получать нулевые значения.
Скользящее окно и выбор размера
Параметр http_req_rate(10s) задаёт длину окна: 10 секунд. Если вы ждёте трафик «рывками», обычно лучше брать чуть большее окно (например 30–60 секунд), чтобы не резать клиентов за короткий всплеск.
Общая идея: чем больше окно, тем мягче реакция на кратковременные пики, но тем дольше злой клиент остаётся «наказанным».
Типичные варианты для API:
- Публичный REST API:
http_req_rate(10s)с лимитом 20–50 RPS. - Внутренний API: 100–300 RPS, иногда без строгих лимитов (только защита от аномалий).
- Эндпоинт логина:
http_req_rate(60s)с небольшим порогом (5–10 запросов в минуту).
Практический приём: сначала соберите метрики по текущему трафику (по логам или мониторингу), а уже потом подбирайте окна и пороги. Слепой выбор значений почти всегда приводит к ложным срабатываниям.
Разные лимиты для разных путей API
Обычно у API нет одного глобального лимита. Примеры:
/api/auth/login— строгий лимит для защиты от брутфорса./api/public/— мягкий лимит, много анонимных клиентов./api/admin/— небольшой трафик, можно ставить жёсткий лимит и дополнительные проверки.
Настроим разные пороги в одном бэкенде:
backend api_backend
stick-table type ip size 200k expire 15m store http_req_rate(10s),http_req_rate(60s)
http-request track-sc0 src
acl path_login path_beg /api/auth/login
acl path_public path_beg /api/public/
acl too_many_login path_login sc0_http_req_rate(60s) gt 20
acl too_many_public path_public sc0_http_req_rate(10s) gt 300
http-request deny status 429 if too_many_login
http-request deny status 429 if too_many_public
server api1 10.0.0.10:8080 check
Здесь:
- В таблице мы храним два счётчика: для 10 и 60 секунд.
- ACL совмещают путь и IP: логин режем по медленному окну, публичный API — по быстрому.
Если вы уже используете HAProxy для балансировки HTTP/2 или gRPC, полезно посмотреть общий обзор работы stick-tables и примеров ACL в статье о настройке HAProxy для HTTP/2 и gRPC.
Лимит не по IP, а по ключу API или токену
Лимит по IP — грубый инструмент. Один IP может принадлежать большому количеству пользователей (NAT, корпоративная сеть). Чаще нужно лимитировать по:
- ключу API (например, в
X-API-Key); - JWT-токену (часто в
Authorization: Bearer ...); - ID клиента в query-параметрах.
Ключевая идея: в stick-table можно хранить не только IP, но и произвольные строки (type string), главное — контролировать длину.
Rate limit по заголовку X-API-Key
backend api_ratelimit_key
stick-table type string len 64 size 500k expire 10m store http_req_rate(30s)
# Забираем ключ из заголовка
http-request set-var(txn.apikey) req.hdr(X-API-Key)
# Не трекаем пустые ключи
acl missing_key var(txn.apikey) -m len 0
http-request deny status 401 if missing_key
# Для валидных ключей считаем rate
http-request track-sc0 var(txn.apikey)
acl too_many_by_key sc0_http_req_rate gt 300
http-request deny status 429 if too_many_by_key
server api1 10.0.0.20:8080 check
Нюансы:
type string len 64— ограничиваем длину ключа, чтобы кто-то не принёс килобайтный мусор и не раздувал память.- Если ключ отсутствует, сразу возвращаем 401, не трогая таблицу.
- Лимит завязан именно на ключ, а не на IP клиента.
Комбинированный ключ: IP + User-Agent или IP + путь
Иногда IP сам по себе — слишком грубый идентификатор, а отдельная строка — наоборот слишком «мелкая». Можно собирать составной ключ из нескольких частей.
Пример: хотим ограничить RPS по тройке «IP + путь + метод» для более тонкого контроля.
backend api_ratelimit_combo
stick-table type string len 128 size 500k expire 10m store http_req_rate(10s)
# Собираем составной ключ
http-request set-var(txn.rate_key) src,,:,method,,:,path
http-request track-sc0 var(txn.rate_key)
acl too_fast sc0_http_req_rate gt 100
http-request deny status 429 if too_fast
server api1 10.0.0.30:8080 check
В реальности лучше опираться на src, если HAProxy — граничный прокси. Если перед ним есть ещё уровень балансировки, сначала стоит аккуратно валидировать доверенные прокси и заголовки типа X-Forwarded-For.
Защита от брутфорса логина
Классический сценарий: атакующий перебирает пароли на /api/auth/login. Нужно ограничить количество неуспешных попыток по IP, но не наказывать нормального пользователя за пару неверных вводов.
Лимит по IP + статусу ответа
Тонкость: считать надо не все запросы, а только неуспешные логины (например, 401/403). Но статус ответа мы узнаём только после обращения к бэкенду. Для этого есть http-response track-sc.
backend api_auth
stick-table type ip size 100k expire 30m store http_err_rate(60s)
# Привязываем IP к таблице
http-request track-sc0 src
acl is_login path_beg /api/auth/login
# На бэкенд пускаем всегда, но следим за ответом
http-response track-sc0 src if is_login status 401 403
acl too_many_failed sc0_http_err_rate gt 20
# Если накопилось много ошибок - режем запросы на входе
http-request deny status 429 if is_login too_many_failed
server auth1 10.0.0.40:8080 check
Здесь важно:
- Используем
http_err_rate(60s)— считаем только ошибки (401/403), а не весь трафик. - Блокируем запросы уже на стадии
http-request, если по IP накопилось слишком много фейлов.
Базовая защита от DDoS API с помощью stick-tables
Серьёзные DDoS-атаки лучше отбивать на сетевом уровне (файрволы, анти-DDoS у провайдера), но stick-tables помогают в случаях, когда атака идёт по HTTP-уровню, с относительно небольшим количеством IP и без чудовищного PPS.
Что можно сделать без тяжёлой артиллерии:
- Ограничить RPS по IP (как в базовом примере).
- Отсекать клиентов, которые постоянно получают 4xx/5xx (похожие на сканеры и ботов).
- Временно «замораживать» особо шумных клиентов (soft ban).
Soft ban с помощью stick-tables
Отдельная таблица может хранить «бан-лист» IP-адресов с таймаутом.
backend api_banlist
stick-table type ip size 100k expire 10m store gpc0
backend api_main
stick-table type ip size 500k expire 10m store http_req_rate(10s),http_err_rate(60s)
# Основной учёт по IP
http-request track-sc0 src
# Вторая таблица - банлист
http-request track-sc1 src table api_banlist
# Если IP уже в банлисте - сразу 429
acl banned sc1_get_gpc0 gt 0
http-request deny status 429 if banned
# Слишком много ошибок - записываем в банлист
acl too_many_errors sc0_http_err_rate gt 50
http-request sc-inc-gpc0(0) src table api_banlist if too_many_errors
server api1 10.0.0.10:8080 check
Здесь используется:
gpc0— простое целочисленное поле (general purpose counter) в таблице банлиста для пометки IP.- Отдельный stick-counter
sc1, указывающий на таблицуapi_banlist.
Схема упрощена, но даёт идею: при превышении порога ошибок IP попадает в бан-таблицу на 10 минут (по expire), и дальше все запросы от него режутся сразу.

Нюансы производительности и памяти
Stick-tables хранятся в оперативной памяти процесса HAProxy. Их легко перегрузить, если бездумно ставить огромные size, длинные строки и десяток полей store.
На что обратить внимание:
size— максимальное количество ключей. Каждому ключу соответствует объект в памяти (десятки байт плюс длина ключа для строк).expire— чем он больше, тем дольше «висят» мёртвые ключи. Для активного публичного API 5–15 минут часто достаточно.- Количество полей
store— каждое поле добавляет немного памяти на запись. Не храните всё подряд.
Лучше несколько специализированных таблиц для разных задач (rate по IP, отдельный банлист, отдельная статистика ошибок), чем одна универсальная помойка с десятком полей.
Отладка stick-tables: CLI и stats
Без отладки rate limit превращается в магию. В HAProxy есть удобные инструменты для просмотра содержимого таблиц и их состояния.
Подключение к runtime socket
Сначала включаем сокет в конфиге:
global
stats socket /run/haproxy/admin.sock mode 600 level admin
Смотрим содержимое таблицы:
echo "show table api_ratelimit" | socat stdio /run/haproxy/admin.sock
Или конкретный ключ:
echo "show table api_ratelimit key 192.0.2.10" | socat stdio /run/haproxy/admin.sock
Так можно увидеть текущий http_req_rate, время до истечения записи, флаги и т. д.
Логи и корреляция
Полезно логировать текущий rate в access-лог, чтобы проще отлаживать пороги. Пример лог-формата:
frontend api_frontend
log-format "%ci:%cp [%t] %ft %b %s %TR/%Tw/%Tc/%Tr/%Tt %ST %B %tsc scur=%scur rate=%[sc0_http_req_rate(10s)] %r"
Это позволяет быстро увидеть, какие клиенты чаще всего упираются в лимиты и что происходило с rate перед ответом 429.
Типичные ошибки и подводные камни
Ошибочный type в stick-table
Если вы используете track-sc0 src, в таблице должен быть type ip. С type string такой track-sc работать не будет. Аналогично, если отслеживаете var(txn.apikey), в таблице должен быть type string.
Слишком жёсткие лимиты
Ставить лимит «с потолка» на прод — верный способ получить случайные 429 для легитимных клиентов. Лучше действовать по шагам:
- Сначала логировать и только помечать тех, кто превышает границы (например, через дополнительный заголовок или поле в логах).
- Посмотреть по логам, сколько реальных клиентов попадает под пороги.
- Только потом включать
denyи постепенно ужесточать правила.
Одинаковый лимит для всех эндпоинтов
Часто разные части API имеют разный профиль нагрузки. Без дифференциации вы можете:
- несправедливо душить дешёвые кешируемые GET;
- одновременно недолимитить тяжёлые POST.
Минимальный апгрейд — разделить лимиты хотя бы по основным путям (/auth, публичные, админка) или по методам (GET отдельно от POST).
Пошаговый чек-лист внедрения rate limit на API в HAProxy
- Собрать статистику текущего трафика: RPS по IP, эндпоинтам, методам и статусам.
- Выделить критичные эндпоинты: логин, тяжёлые POST, публичные ресурсы.
- Спроектировать схемы ключей: IP, ключ API, комбинированные варианты.
- Создать одну-две
stick-tableдля пилота, включить только логирование безdeny. - Наблюдать несколько дней, корректировать пороги и окна.
- Включить
deny 429, но сначала только для самых рискованных путей (логин, админка). - Подключить мониторинг по метрикам: число 429, загрузка бэкендов, размер таблиц, частота заполнения до
size.
Заключение
Stick-tables в HAProxy — это не только про sticky-сессии. Для API это мощный встроенный механизм rate limit и простая поведенческая защита от шумных клиентов, брутфорса и части HTTP-слоя DDoS.
Ключ к успеху — аккуратно спроектированные ключи (IP, токены, комбинированные варианты), разумные размеры и expire таблиц, а также обязательная отладка и мониторинг через runtime socket и логи. Так вы разгружаете backend-сервисы, снижаете риск «само-DDoS» из-за багов в клиентах и повышаете надёжность API без ввода дополнительных тяжёлых компонентов в инфраструктуру.
Если вы только планируете выносить API за балансировщик, имеет смысл сразу подобрать тариф на виртуальном хостинге или VDS с достаточным запасом по памяти под кэш stick-tables и логи — это упростит масштабирование и последующую донастройку rate limit.


