Rate limiting is a fundamental defense mechanism for public APIs. It shields your application servers from heavy scrapers, brute force login attempts, and denial-of-service (DDoS) traffic spikes. However, in standard Django setups, rate limits are typically defined as static python decorators (like @ratelimit(key='ip', rate='10/m')).
The problem with hardcoded limits is flexibility: if your API is under attack, or if a partner hits a quota threshold, you have to modify your code, commit, run CI/CD pipelines, and redeploy. The solution is Database-Driven Dynamic Rate Limiting configured entirely through the Django Admin panel.
The System Design
To implement this, we'll design a custom Django middleware that intercepts every incoming request, matches it against rate limit configurations stored in the database, and uses Django's cache layer to track rate counts. To make this production-ready, we must cache the database configurations in-memory to prevent rate-limiting queries from overloading our database on every request.
Step 1: Defining the Database Schema
We'll create a RateLimitConfig model to store our dynamic rules. Each rule specifies a name, priority, URL regex pattern, HTTP methods, rate limit string (e.g. 5/m or 100/h), and rate key (e.g. IP, User ID, or user-with-IP fallback):
from django.db import models
class RateLimitConfig(models.Model):
class KeyType(models.TextChoices):
IP = 'ip', 'IP Address'
USER = 'user', 'Authenticated User'
USER_OR_IP = 'user_or_ip', 'User ID (Fallback to IP)'
name = models.CharField(max_length=100, unique=True)
enabled = models.BooleanField(default=True)
priority = models.PositiveIntegerField(default=10)
url_pattern = models.CharField(max_length=255, help_text="Regex pattern (e.g. '^/api/vins/')")
http_methods = models.CharField(max_length=10, default='ALL', help_text="HTTP method or 'ALL'")
rate_limit = models.CharField(max_length=20, help_text="Format: count/period (e.g. '20/m', '500/h')")
rate_key = models.CharField(max_length=20, choices=KeyType.choices, default=KeyType.IP)
class Meta:
ordering = ['priority', 'name']
Step 2: Building the Dynamic Middleware
Next, we write a custom middleware that runs dynamic regex matches against the cached rules. We will use the underlying core engine of django-ratelimit to record and check hits in the cache:
import re
from django.core.cache import cache
from django.http import JsonResponse
from django_ratelimit.core import is_ratelimited
class DynamicRateLimitMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# 1. Fetch active configurations from cache (cache for 60 seconds to protect DB)
configs = self._get_cached_configs()
# 2. Match request against active configurations
for config in configs:
if self._should_apply_limit(request, config):
# Closure to fetch the correct tracking key
def get_key(group, req, rate_key_type=config.rate_key):
return self._get_rate_limiting_key(req, rate_key_type)
# Check dynamic rate limit using django-ratelimit core
is_limited = is_ratelimited(
request=request,
group=config.name,
key=get_key,
rate=config.rate_limit,
increment=True
)
if is_limited:
return JsonResponse(
{"error": "Rate limit exceeded", "detail": f"Limit: {config.rate_limit}"},
status=429
)
# Apply only the first matching configuration
break
return self.get_response(request)
def _get_cached_configs(self):
cache_key = "rate_limit_configs_all"
configs = cache.get(cache_key)
if configs is None:
configs = list(RateLimitConfig.objects.filter(enabled=True))
cache.set(cache_key, configs, 60) # Cache for 60 seconds
return configs
def _should_apply_limit(self, request, config):
if config.http_methods != 'ALL' and request.method != config.http_methods:
return False
return bool(re.search(config.url_pattern, request.path))
def _get_rate_limiting_key(self, request, rate_key_type):
if rate_key_type == 'user' and request.user.is_authenticated:
return f"user:{request.user.id}"
elif rate_key_type == 'user_or_ip':
if request.user.is_authenticated:
return f"user:{request.user.id}"
return f"ip:{request.META.get('REMOTE_ADDR')}"
return f"ip:{request.META.get('REMOTE_ADDR')}"
Key Performance Takeaways
- Protect the Database: Querying the database to fetch active rules on every HTTP request creates massive overhead. Always cache your configurations in-memory or in Redis for at least 60 seconds.
- Validate Regex Pre-Insertion: Add validation to the admin panel (
clean()method in forms) to ensure that users do not input broken regex structures that could crash the middleware. - Use Cache Brokers: Ensure Django is configured with a high-performance cache broker (like Redis or Memcached) to handle the sub-millisecond increment lookups.
Implementing a dynamic, database-driven rate limiting architecture gives your DevSecOps team the agility to adjust API traffic limits on-the-fly, locking down malicious traffic without single codebase redeployments.