Rate Limit Django Views With django-ratelimit
Add application-level rate limiting to Django views with django-ratelimit: protect login, signup, password reset, and search, key limits by IP, user, or field, use a shared Redis cache, get the real client IP behind proxies, and return helpful 429 responses.
Problem statement
Bots hammer your login form, password reset endpoint, and signup page; a scraper hits search thousands of times a minute. Nginx limit_req can cap requests per IP, but it knows nothing about usernames, accounts, or which form field was submitted. django-ratelimit adds limits inside Django, where you can key them by IP, user, a POST field such as the email being reset, or any custom function, and respond in a way that fits the view.
pip install django-ratelimitand make sureCACHES["default"]is a shared backend such as Redis.- Decorate views:
@ratelimit(key="ip", rate="10/m", method="POST", block=True). - Use multiple keys for logins: per IP and per submitted username (
key="post:username"). - Set
RATELIMIT_IP_META_KEYor a callable so limits use the real client IP behind proxies. - Return a clear 429 page or JSON, and log rate-limit hits for monitoring.
Step-by-step solution
1. Install and configure the cache
INSTALLED_APPS += ["django_ratelimit"]
CACHES = {"default": {"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": os.environ["REDIS_URL"]}}
RATELIMIT_USE_CACHE = "default"
RATELIMIT_VIEW = "core.views.ratelimited"
MIDDLEWARE += ["django_ratelimit.middleware.RatelimitMiddleware"]
The library counts requests in the cache. A local-memory cache would give each worker process its own counters, multiplying the effective limit; Django's system checks warn about unsuitable cache backends. RatelimitMiddleware turns Ratelimited exceptions into calls to RATELIMIT_VIEW.
2. Protect sensitive views
from django_ratelimit.decorators import ratelimit
@ratelimit(key="ip", rate="20/m", method="POST", block=True)
@ratelimit(key="post:username", rate="5/m", method="POST", block=True)
def login_view(request):
...
@ratelimit(key="ip", rate="5/h", method="POST", block=True)
@ratelimit(key="post:email", rate="3/h", method="POST", block=True)
def password_reset(request):
...
@ratelimit(key="user_or_ip", rate="60/m", block=True)
def search(request):
...
Stacking keys catches both a single IP trying many usernames and many IPs (a botnet) trying one username.
3. Use the real client IP
Behind Nginx, a load balancer, or Cloudflare, REMOTE_ADDR is the proxy's address, so every user shares one limit. Configure where the client IP comes from:
RATELIMIT_IP_META_KEY = "HTTP_X_REAL_IP" # set by your Nginx from a trusted source
# or a callable returning the IP after validating the proxy chain:
# RATELIMIT_IP_META_KEY = "core.net.client_ip"
Only trust headers that your own proxy sets and overwrites; otherwise attackers can spoof X-Forwarded-For to bypass limits. With Cloudflare, use CF-Connecting-IP and restrict origin access to Cloudflare. Limits on IPv6 can key on the /64 prefix (RATELIMIT_IPV6_MASK).
4. Respond well
from django.http import JsonResponse
from django.shortcuts import render
def ratelimited(request, exception):
if request.headers.get("Accept", "").startswith("application/json"):
return JsonResponse({"detail": "Too many requests. Try again later."}, status=429)
return render(request, "429.html", status=429)
For forms, block=False and checking request.limited lets you show a captcha instead of blocking.
5. Monitor and tune
Log blocked requests with the key type and view, and chart them. Spikes show attacks; a steady stream from real users means a limit is too tight. Combine with django-axes for account lockout policies and with Nginx limits for cheap volume filtering before requests reach Django.
Explanation
django-ratelimit computes a key for each request (from the IP, user, a header, a form field, or your function), builds a cache key from the view, key, and time window, and increments a counter. When the counter exceeds the rate within the window, the request is marked limited and, with block=True, a Ratelimited exception is raised. Because the counter lives in the shared cache, limits apply across all workers and servers.
Edge cases / notes
- Shared IPs. Offices, schools, and mobile carriers share IPs; per-IP limits on login should not be so tight that a building gets locked out.
- Cache outages. If the cache is down, decide whether to fail open (no limits) or closed; most sites fail open to stay available.
- APIs. For Django REST Framework, its built-in throttling is a better fit.
Next steps
- Filter volume earlier with Rate Limit Django With Nginx.
- Lock accounts after failures with Django Axes Brute Force Protection.
- Get IPs right with Fix Django Wrong Client IP Behind a Proxy.
FAQ
Do I need Redis for django-ratelimit?
You need a shared cache. Redis or Memcached work; local memory caches do not across processes.
Why does everyone hit the limit at once?
The limit is keyed on the proxy's IP. Configure the real client IP header.
Should I use django-ratelimit or Nginx limits?
Both. Nginx is cheap for volume; django-ratelimit understands usernames and form fields.
How do I rate limit DRF APIs?
Use DRF's throttling classes, which integrate with authentication and views.