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.

django-ratelimit flowRequestlogin POSTKeyIP, user, fieldCountershared cacheOver limit429 or blockLimits keyed by what the attacker varies, counted in a shared cache.
  1. pip install django-ratelimit and make sure CACHES["default"] is a shared backend such as Redis.
  2. Decorate views: @ratelimit(key="ip", rate="10/m", method="POST", block=True).
  3. Use multiple keys for logins: per IP and per submitted username (key="post:username").
  4. Set RATELIMIT_IP_META_KEY or a callable so limits use the real client IP behind proxies.
  5. 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.

view and suggested keysKeysExample rateLoginip + post:username20/m and 5/mPassword resetip + post:email5/h and 3/hSignupip10/hSearch, APIuser_or_ip60/mContact formip5/hKey by whatever an attacker would vary or repeatdjango-deployment.com
Start generous and tighten with data from logged hits.

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.

from edge to accountCDN or WAF rulesbot reputation, floodsNginx limit_reqcheap per-IP capdjango-ratelimitIP, username, emaildjango-axes lockoutper account failuresCheap layers filter volume; Django layers understand accountsdjango-deployment.com
Each layer catches attacks the others cannot see.

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

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.

Keep going