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

  1. 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.
  2. 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.
  3. 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.