| name | django-ratelimit |
| description | Django rate limiting patterns using django-ratelimit with Redis backend for protecting views from abuse and spam. Trigger: When implementing rate limiting, spam protection, or throttling in Django views.
|
| license | Apache-2.0 |
| metadata | {"author":"Carlos","version":"1.0","scope":["root"],"auto_invoke":"Implementing rate limiting/spam protection"} |
| allowed-tools | Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task |
What is django-ratelimit?
django-ratelimit provides simple, flexible rate limiting for Django views. It's essential for:
- Spam prevention: Limit form submissions (surveys, contact forms, registrations)
- API protection: Throttle API endpoints to prevent abuse
- Brute-force mitigation: Slow down login attempts
- Resource protection: Prevent DoS attacks on expensive operations
Key Benefits:
- Redis-backed (persistent, distributed)
- Decorator-based (easy to apply)
- Flexible key functions (IP, user, custom)
- Block or track mode
- Compatible with class-based and function-based views
Installation
pip install django-ratelimit redis
REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0")
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": REDIS_URL,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
"KEY_PREFIX": "ratelimit",
"TIMEOUT": 900,
}
}
Environment Variables:
REDIS_URL=redis://localhost:6379/0
Basic Usage (REQUIRED)
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from django_ratelimit.decorators import ratelimit
@ratelimit(key='ip', rate='5/m', method='POST', block=False)
def submit_form(request: HttpRequest) -> HttpResponse:
"""Allow 5 POST requests per minute per IP."""
if getattr(request, 'limited', False):
return render(request, 'rate_limited.html', status=429)
return render(request, 'success.html')
Decorator Parameters:
| Parameter | Description | Example |
|---|
key | What to track | 'ip', 'user', 'user_or_ip', function |
rate | Limit format | '5/m' (5 per minute), '100/h', '1000/d' |
method | HTTP methods | 'POST', 'GET', ['POST', 'PUT'], ALL |
block | Block requests | True (403 error), False (set request.limited) |
Rate Format:
'5/s'
'10/m'
'100/h'
'1000/d'
Key Functions (REQUIRED)
Built-in Keys
from django_ratelimit.decorators import ratelimit
@ratelimit(key='ip', rate='5/m')
def view_by_ip(request):
pass
@ratelimit(key='user', rate='10/m')
def view_by_user(request):
pass
@ratelimit(key='user_or_ip', rate='5/m')
def view_by_user_or_ip(request):
pass
@ratelimit(key='get:q', rate='10/m')
@ratelimit(key='post:email', rate='3/m')
def view_by_param(request):
pass
@ratelimit(key='header:x-api-key', rate='100/h')
def api_endpoint(request):
pass
Custom Key Functions
from typing import Optional
def get_key_ip_and_unit(group: str, request: HttpRequest) -> Optional[str]:
"""
Custom rate limit key combining IP and unit ID.
Use case: Limit survey submissions per IP per unit.
"""
unit_id = request.POST.get('unit_id') or request.GET.get('unit_id')
if not unit_id:
return None
ip = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')[0].strip()
if not ip:
ip = request.META.get('REMOTE_ADDR', '')
return f"{ip}:{unit_id}"
@ratelimit(key=get_key_ip_and_unit, rate='1/15m', method='POST', block=False)
def submit_survey(request: HttpRequest, transit_number: str) -> HttpResponse:
"""Limit 1 survey submission per IP per unit every 15 minutes."""
if getattr(request, 'limited', False):
return render(request, , {
: ,
}, status=)
Block vs Track Mode
Block Mode (block=True)
@ratelimit(key='ip', rate='5/m', method='POST', block=True)
def strict_view(request: HttpRequest) -> HttpResponse:
"""User gets 403 error if rate limit exceeded."""
return render(request, 'form.html')
Use when:
- You want automatic rejection
- Simple protection is enough
- You don't need custom error messages
Track Mode (block=False) - RECOMMENDED
@ratelimit(key='ip', rate='5/m', method='POST', block=False)
def flexible_view(request: HttpRequest) -> HttpResponse:
"""Custom handling of rate limit exceeded."""
if getattr(request, 'limited', False):
return render(request, 'rate_limited.html', {
'wait_time': 1,
'retry_after': 60,
}, status=429)
return render(request, 'form.html')
Use when:
- You want custom error messages
- Need to log rate limit events
- Want to show "try again in X minutes"
- Different handling per view
Real-World Patterns
Survey Submission Protection
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render, get_object_or_404
from django_ratelimit.decorators import ratelimit
from apps.transport.models import Unit
def get_ratelimit_key_ip_and_unit(group: str, request: HttpRequest) -> str:
"""Rate limit key: IP + Unit ID."""
unit_id = request.resolver_match.kwargs.get('transit_number', '')
ip = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')[0].strip()
if not ip:
ip = request.META.get('REMOTE_ADDR', '')
return f"{ip}:{unit_id}"
@ratelimit(key=get_ratelimit_key_ip_and_unit, rate='1/15m', method='POST', block=False)
def submit_survey(request: HttpRequest, transit_number: str) -> HttpResponse:
"""
Submit survey with rate limiting.
Limit: 1 submission per IP per unit every 15 minutes.
"""
unit = get_object_or_404(Unit, transit_number=transit_number)
if getattr(request, 'limited', False):
return render(request, 'interview/rate_limited.html', {
: unit,
: ,
}, status=)
request.method == :
form = SurveyForm(request.POST)
form.is_valid():
form.save()
redirect()
:
form = SurveyForm()
render(request, , {
: form,
: unit,
})
Login Attempt Protection
@ratelimit(key='ip', rate='5/h', method='POST', block=False)
def login_view(request: HttpRequest) -> HttpResponse:
"""Limit failed login attempts to 5 per hour per IP."""
if getattr(request, 'limited', False):
return render(request, 'auth/rate_limited.html', {
'message': 'Too many login attempts. Try again in 1 hour.',
}, status=429)
API Endpoint Protection
@ratelimit(key='user_or_ip', rate='100/h', method='ALL', block=False)
def api_endpoint(request: HttpRequest) -> HttpResponse:
"""Limit API calls to 100 per hour per user/IP."""
if getattr(request, 'limited', False):
return JsonResponse({
'error': 'Rate limit exceeded',
'retry_after': 3600,
}, status=429)
Class-Based Views
from django.views.generic import FormView
from django.utils.decorators import method_decorator
from django_ratelimit.decorators import ratelimit
@method_decorator(ratelimit(key='ip', rate='5/m', method='POST', block=False), name='post')
class ContactFormView(FormView):
"""Rate-limited contact form."""
template_name = 'contact.html'
form_class = ContactForm
def post(self, request, *args, **kwargs):
if getattr(request, 'limited', False):
return render(request, 'rate_limited.html', status=429)
return super().post(request, *args, **kwargs)
Rate Limit Response Template
{% extends "base.html" %}
{% block content %}
<div class="error-container">
<h1>Too Many Requests</h1>
<p>You've exceeded the rate limit for this action.</p>
<p>Please wait {{ wait_minutes }} minute(s) before trying again.</p>
<a href="{% url 'home' %}">Go Home</a>
</div>
{% endblock %}
Testing Rate Limits
from django.test import TestCase, Client
from django.urls import reverse
class RateLimitTestCase(TestCase):
def test_survey_rate_limit(self):
"""Test survey submission rate limit."""
client = Client()
url = reverse('submit_survey', kwargs={'transit_number': 'ABC123'})
response = client.post(url, {'rating': 5})
self.assertEqual(response.status_code, 200)
response = client.post(url, {'rating': 5})
self.assertEqual(response.status_code, 429)
Common Commands
docker run -d -p 6379:6379 redis:alpine
redis-cli ping
redis-cli KEYS "ratelimit:*"
redis-cli FLUSHDB
redis-cli DEL "ratelimit:rl:ip:127.0.0.1"
Best Practices Checklist
ALWAYS:
- ✅ Use Redis backend for persistent, distributed rate limiting
- ✅ Use
block=False for custom error handling
- ✅ Return HTTP 429 (Too Many Requests) status code
- ✅ Show user-friendly error messages with wait times
- ✅ Use composite keys for complex rate limits (IP + resource)
- ✅ Set reasonable limits (1 per 15 minutes for forms is good)
- ✅ Test rate limits in development
- ✅ Log rate limit events for monitoring
- ✅ Use
user_or_ip for authenticated + anonymous users
- ✅ Apply rate limits to POST/PUT/DELETE (not GET)
NEVER:
- ❌ Use in-memory cache for rate limiting (not persistent)
- ❌ Set limits too low (frustrates legitimate users)
- ❌ Forget to handle
request.limited when block=False
- ❌ Rate limit GET requests (except expensive searches)
- ❌ Use rate limiting as primary spam protection (use reCAPTCHA too)
- ❌ Return 403 for rate limits (use 429)
- ❌ Forget to configure Redis in production
Troubleshooting
Rate limit not working
from django.core.cache import cache
cache.set('test', 'value', 60)
print(cache.get('test'))
import logging
logger = logging.getLogger(__name__)
@ratelimit(key='ip', rate='5/m', method='POST', block=False)
def my_view(request):
logger.info(f"Rate limited: {getattr(request, 'limited', False)}")
Custom key function not called
def get_key(group: str, request: HttpRequest) -> Optional[str]:
return "some_key"
def get_key(request):
return "some_key"
Rate limit not cleared after expiry
redis-cli TTL "ratelimit:rl:ip:127.0.0.1"
redis-cli DEL "ratelimit:rl:ip:127.0.0.1"
Resources