Throttling в DRF: сравниваем варианты и выбираем
Встроенные классы throttling в Django REST Framework решают базовую задачу, но ломаются на распределённом трафике и хитрых лимитах. Разбираем, где предел стандартных решений и когда пора уходить в отдельный слой.

Задача звучит просто: не дать одному клиенту забить API. В Django REST Framework для этого есть готовые классы AnonRateThrottle, UserRateThrottle и ScopedRateThrottle. Вы прописываете строку вида 100/hour в настройках — и защита работает. Проблема начинается, когда приложение выезжает за один процесс: у вас три инстанса за балансировщиком, лимит счётся в памяти каждого по отдельности, и клиент получает не 100 запросов в час, а 300. Дальше — про то, какие варианты есть, чем каждый платит и когда штатного механизма уже не хватает.
Материал для DRF 3.14+ и Django 4.2/5.x. Инвалидацию и прогрев кеша не трогаем — говорим только про ограничение частоты запросов.
Как штатный throttling считает запросы
Встроенные классы наследуются от SimpleRateThrottle. Механизм один: класс формирует ключ кеша (для UserRateThrottle это ID пользователя, для AnonRateThrottle — IP-адрес), складывает в кеш список временных меток запросов и на каждом новом запросе выкидывает те, что вышли за окно. Если меток больше лимита — возвращает 429 Too Many Requests.
Ключевое слово здесь — кеш. По умолчанию DRF использует бэкенд кеша Django с именем default. Если это LocMemCache (память процесса), то каждый инстанс приложения ведёт свой счёт. Отсюда и утечка лимита при горизонтальном масштабировании.
Штатный throttling в DRF настолько же распределённый, насколько распределён ваш бэкенд кеша. На LocMemCache он не распределён вообще.
Формат строки лимита
Лимит задаётся строкой число/период, где период — second, minute, hour или day (допустимы сокращения s, m, h, d). Составных окон вроде «10 в секунду и 1000 в час одновременно» одна строка не выражает — для этого вешают несколько классов throttling сразу.
Четыре варианта, между которыми выбирают
Вариант 1. Встроенные классы на LocMemCache
Что выигрываете: ноль внешних зависимостей, работает из коробки. Что платите: лимит корректен только на одном процессе. При нескольких воркерах Gunicorn или нескольких подах лимит умножается на их число. Годится для дев-окружения и одиночного инстанса, не годится для прода с масштабированием.
Вариант 2. Встроенные классы на Redis/Memcached
Тот же код, но кеш вынесен в общее хранилище. Все инстансы видят один счётчик, лимит становится честным. Что платите: сетевой поход в кеш на каждый запрос и зависимость от доступности Redis. Если Redis лёг, поведение зависит от бэкенда — часть падает с ошибкой, часть тихо пропускает запрос. Это самый частый рабочий выбор для средних нагрузок.
Вариант 3. Кастомный класс throttle
Когда нужен нестандартный ключ (например, лимит по API-ключу партнёра, а не по пользователю) или динамический лимит из базы. Наследуетесь от SimpleRateThrottle и переопределяете get_cache_key. Что платите: свой код, который надо тестировать, и риск ошибиться в формировании ключа так, что разные клиенты попадут в один счётчик.
Вариант 4. Throttling на уровне infrastructure
Ограничение выносится в Nginx (limit_req), API Gateway или отдельный сервис. Что выигрываете: защита срабатывает до того, как запрос дошёл до Python, — экономите worker'ы и защищаетесь от простого флуда. Что платите: не видите бизнес-контекст (кто пользователь, какой у него тариф), лимиты грубее. На практике этот слой ставят вместе со штатным DRF, а не вместо.
Таблица сравнения
| Вариант | Распределённость | Знает пользователя | Гибкость лимита | Цена |
|---|---|---|---|---|
| Встроенные + LocMemCache | Нет | Да | Средняя | Ноль |
| Встроенные + Redis | Да | Да | Средняя | Redis + сеть на запрос |
| Кастомный класс | Зависит от кеша | Любой ключ | Высокая | Свой код и тесты |
| Nginx / Gateway | Да | Нет | Низкая (по IP/пути) | Конфиг инфры |
Как включить распределённый вариант
Практически всегда правильный старт — вариант 2. Настраивается в двух местах.
Бэкенд кеша на Redis (Django 4.2+ умеет Redis встроенным бэкендом django.core.cache.backends.redis.RedisCache):
# settings.py
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
}
}
REST_FRAMEWORK = {
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"anon": "20/minute",
"user": "120/minute",
},
}
Разные лимиты на разные вьюхи задаёт ScopedRateThrottle. На вью прописываете throttle_scope, а лимиты для скоупов — в тех же DEFAULT_THROTTLE_RATES:
# views.py
from rest_framework.throttling import ScopedRateThrottle
from rest_framework.views import APIView
from rest_framework.response import Response
class PasswordResetView(APIView):
# медленный дорогой эндпоинт — отдельный жёсткий лимит
throttle_classes = [ScopedRateThrottle]
throttle_scope = "password_reset"
def post(self, request):
# ... логика сброса пароля
return Response({"status": "ok"})
# settings.py — добавляем скоуп в тот же словарь
REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["password_reset"] = "5/hour"
Кастомный ключ: лимит по API-ключу партнёра
Если у вас B2B-интеграции и каждый партнёр присылает свой ключ в заголовке, считать лимит по пользователю Django бессмысленно — пользователя может не быть. Формируете ключ сами:
# throttling.py
from rest_framework.throttling import SimpleRateThrottle
class PartnerKeyThrottle(SimpleRateThrottle):
scope = "partner"
def get_cache_key(self, request, view):
api_key = request.headers.get("X-Partner-Key")
if not api_key:
# без ключа этот throttle не применяем — вернёт None,
# запрос пройдёт мимо данного класса
return None
# cache_format подставит scope и ident в единый ключ кеша
return self.cache_format % {
"scope": self.scope,
"ident": api_key,
}
Лимит для скоупа partner добавляете в DEFAULT_THROTTLE_RATES, класс подключаете на нужные вьюхи через throttle_classes. Тонкость: если get_cache_key вернул None, DRF считает, что throttle к этому запросу неприменим, и пропускает его. Легко случайно раздать безлимит всем, кто не прислал ключ, — держите это в голове.
Что ломается на практике
- IP за прокси.
AnonRateThrottleберёт IP изrequest.META. За балансировщиком все запросы придут с одного IP прокси — весь анонимный трафик попадёт в один счётчик и упрётся в лимит. Нужно правильно настроить получение реального IP (заголовок вродеX-Forwarded-For) и доверять ему только от своего прокси. - Redis упал. Штатный throttle не рассчитан на отказ кеша. Проверьте, что происходит при недоступности Redis в вашей связке, и решите заранее — падать (fail closed) или пропускать (fail open).
- Лимит не срабатывает вообще. Частая причина —
DEFAULT_THROTTLE_CLASSESпрописан, аDEFAULT_THROTTLE_RATESдля нужного скоупа нет. Класс без ставки молча ничего не ограничивает. - Заголовок Retry-After. DRF при 429 отдаёт
Retry-Afterс числом секунд до сброса. Клиенты, которые его игнорируют и долбят сразу, — отдельная головная боль, но это уже вопрос к клиенту.
Когда что выбирать
- Один инстанс, внутренний сервис — встроенные классы на любом кеше, можно LocMemCache.
- Прод с горизонтальным масштабированием — встроенные классы плюс общий Redis. База для большинства.
- Нестандартный субъект лимита (партнёр, тариф, устройство) — кастомный класс поверх Redis.
- Защита от грубого флуда и экономия воркеров —
limit_reqв Nginx дополнительно к DRF, а не вместо.
Штатный throttling DRF — это не rate limiter промышленного уровня со скользящим окном и атомарными операциями. Для честного распределённого счётчика под высокой нагрузкой многие уходят на связку с Redis и Lua-скриптом или на отдельный сервис. Но для 90% API встроенных классов на общем Redis достаточно.
Что сделать дальше
Первым делом проверьте, какой у вас бэкенд кеша в проде: если LocMemCache и больше одного воркера — ваши лимиты уже не работают так, как вы думаете. Дальше настройте корректное получение клиентского IP за прокси, иначе анонимный throttle либо блокирует всех, либо не блокирует никого. Мерьте долю ответов 429 в мониторинге: резкий рост — либо атака, либо слишком жёсткий лимит, который бьёт по легитимным клиентам. Точные имена классов и параметры сверяйте с разделом Throttling в документации DRF для вашей версии.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: Django REST Framework — Throttling, Django — Cache framework (Redis backend)
Частые вопросы
У меня не 3.14, а более старая версия DRF — код подойдёт?
Лимит не срабатывает, хотя класс подключён — что не так?
Почему при трёх инстансах клиент делает втрое больше запросов, чем в лимите?
Throttling DRF защитит от полноценной DDoS-атаки?
Что происходит, если Redis недоступен?
Как сделать одновременно лимит в секунду и в час?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.