Configure Django REST Framework Throttling for Production
Set up Django REST Framework throttling that works in production: shared Redis cache, AnonRateThrottle and UserRateThrottle defaults, ScopedRateThrottle per endpoint, correct client IPs behind proxies with NUM_PROXIES, Retry-After headers, and custom throttles per API key.
Problem statement
Your Django REST Framework API needs limits: anonymous clients should not scrape everything, authenticated users should not overwhelm expensive endpoints, and partners should get the quota they pay for. DRF includes throttling classes, but the defaults quietly fail in production: throttle counters in local memory are per process, every client looks like the load balancer's IP, and burst limits on expensive endpoints are missing.
- Use a shared cache (Redis) as the default cache, or point throttles at one explicitly.
- Set defaults:
AnonRateThrottle(for example100/hour) andUserRateThrottle(1000/hour). - Add
ScopedRateThrottlewiththrottle_scopeon expensive endpoints such as exports and search. - Set
NUM_PROXIESso DRF identifies anonymous clients by their real IP. - Write a custom throttle for API keys or plans, and make sure 429 responses include
Retry-After.
Step-by-step solution
1. Configure defaults
REST_FRAMEWORK = {
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
"rest_framework.throttling.ScopedRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"anon": "100/hour",
"user": "1000/hour",
"exports": "10/hour",
"search": "60/minute",
"burst": "30/minute",
},
"NUM_PROXIES": 1,
}
CACHES = {"default": {"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": os.environ["REDIS_URL"]}}
Throttle classes store request history in django.core.cache.cache by default. With a local-memory cache, each Gunicorn worker counts separately, so the effective limit is the configured rate times the number of workers across all servers.
2. Throttle expensive endpoints by scope
class ExportViewSet(viewsets.ViewSet):
throttle_scope = "exports"
class SearchView(APIView):
throttle_scope = "search"
ScopedRateThrottle applies only to views that set throttle_scope, and counts per user (or IP for anonymous clients) per scope.
3. Identify clients correctly behind proxies
For anonymous throttling, DRF uses REMOTE_ADDR, or the X-Forwarded-For header when NUM_PROXIES is set. NUM_PROXIES = 1 means "one trusted proxy in front": DRF takes the address that proxy appended. Count every proxy you control (CDN plus load balancer plus Nginx may be 2 or 3). Getting this wrong either throttles everyone together (too low) or lets clients spoof addresses (too high).
4. Custom throttles for API keys and plans
from rest_framework.throttling import SimpleRateThrottle
class PlanRateThrottle(SimpleRateThrottle):
scope = "plan"
RATES = {"free": "1000/day", "pro": "100000/day"}
def get_cache_key(self, request, view):
account = getattr(request, "account", None)
if account is None:
return None # not throttled by this class
self.rate = self.RATES[account.plan]
self.num_requests, self.duration = self.parse_rate(self.rate)
return f"throttle_plan_{account.pk}"
Add it to DEFAULT_THROTTLE_CLASSES or per view. Return None from get_cache_key to skip throttling, for example for internal service accounts.
5. Responses and headers
DRF returns 429 with a Retry-After header computed from the throttle window, and a detail message such as "Request was throttled. Expected available in 37 seconds." Document limits for API consumers, and consider adding X-RateLimit-Remaining style headers with a small middleware if clients need them. Log throttled requests to spot abusive clients and to tune rates.
Explanation
DRF throttles keep a list of request timestamps per cache key and check, on each request, how many fall within the window. If the count reaches the limit, the request is denied with 429 and a wait time. Because state lives in the cache, the cache must be shared across processes for limits to mean anything. Client identity for anonymous requests depends on proxy configuration, which is why NUM_PROXIES matters as much as the rates.
Edge cases / notes
- Race conditions. DRF's throttles are not strictly atomic under heavy concurrency; for strict quotas (billing), use a Redis-atomic counter.
- Cache key growth. Each client creates keys with TTLs; Redis memory grows with the number of distinct clients, so monitor it.
- Edge limits. Combine with Nginx or CDN rate limiting so floods never reach Django.
Next steps
- Limit non-API views with Rate Limit Django Views With django-ratelimit.
- Make the API faster with Optimize Django REST Framework Performance.
- Review the rest with the Django API Production Checklist.
FAQ
Why does DRF throttling not work with multiple workers?
The default cache is per process. Configure Redis or another shared cache.
What is NUM_PROXIES?
The number of trusted proxies in front of Django. DRF uses it to pick the real client IP from X-Forwarded-For.
How do I throttle one expensive endpoint more strictly?
Set throttle_scope on the view and a matching rate with ScopedRateThrottle.
Can I give different limits per customer plan?
Yes, with a custom SimpleRateThrottle subclass that picks the rate from the account.