Короткий ответ
Нужны лимиты на нескольких уровнях одновременно — per-user/per-API-key (справедливое распределение между клиентами), per-IP (защита от анонимных/нелегитимных источников) и глобальный лимит на уровне всего сервиса (защита инфраструктуры в целом) — с централизованным хранилищем счётчиков (например, Redis) для корректной работы при множестве инстансов сервиса; конкретные пороги и алгоритм (token bucket, sliding window) выбираются исходя из реальной емкости системы и бизнес-тарифов.
Как это работает подробнее
в распределённой системе за балансировщиком счётчики лимитов не могут храниться в памяти одного процесса — нужно общее атомарное хранилище, иначе реальный лимит клиента фактически умножается на число инстансов, через которые пройдут его запросы. Per-user лимит защищает от того, чтобы один клиент "съел" ресурсы, предназначенные для всех; глобальный лимит защищает от ситуации, когда суммарная легитимная нагрузка от множества клиентов (каждый в рамках своего лимита) всё равно превышает реальную ёмкость инфраструктуры.
Пример кода
# Схематичный пример многоуровневой проверки лимита с Redis (token bucket через INCR + EXPIRE)
def check_rate_limits(redis_client, user_id: str, ip: str) -> tuple[bool, str]:
checks = [
(f"rl:user:{user_id}", 1000, 60, "per-user limit exceeded"),
(f"rl:ip:{ip}", 2000, 60, "per-ip limit exceeded"),
("rl:global", 500_000, 60, "global capacity limit exceeded"),
]
for key, limit, window, message in checks:
current = redis_client.incr(key)
if current == 1:
redis_client.expire(key, window)
if current > limit:
return False, message
return True, ""Что использовать на практике
- Рекомендация. разные уровни лимитов должны быть согласованы между собой и с реальной capacity системы, определённой через нагрузочное тестирование, а не подобраны произвольно; крупным клиентам с легитимно высокой нагрузкой стоит предлагать отдельные более высокие лимиты по договорённости (tiered rate limiting) или выделенную инфраструктуру, а не одинаковый лимит для всех. При исчерпании лимита важно возвращать информативный 429 с
Retry-After, чтобы клиенты могли адаптивно снижать частоту запросов, а не "стучаться" агрессивнее. Компромисс: более тонкая многоуровневая схема (per-user + per-IP + глобальный, с разными тарифами для разных клиентов) лучше защищает инфраструктуру и справедливее распределяет ресурсы, но требует заметно более сложной инфраструктуры (централизованное хранилище счётчиков с низкой задержкой, согласование порогов между уровнями, мониторинг срабатываний) и усложняет диагностику при отладке ("почему именно этот запрос получил 429"); простая единая схема (один лимит на всех) проще эксплуатировать, но не защищает от описанных в вопросе сценариев и может быть одновременно слишком строгой для мелких клиентов и слишком мягкой для агрессивных крупных. Критерий выбора — распределение клиентов по объёму трафика, коммерческая модель (есть ли смысл в платных тарифах с разными лимитами) и реальная наблюдаемая ёмкость инфраструктуры.
Подводные камни
- Риски. внедрять только per-IP лимит, который легко обходится через множество IP (NAT, облачные провайдеры с большим пулом адресов) и несправедливо наказывает многих легитимных пользователей за одним IP (например, офис за одним NAT); не предусматривать глобальный "предохранитель" уровня инфраструктуры, полагаясь только на сумму индивидуальных лимитов, что не защищает от ситуации одновременного пика у многих клиентов сразу.