Throttling
Throttling is similar to permissions, but instead of determining if a user is authorized, it determines how often they can access the API. Throttling indicates a temporary state and is used to control the rate of requests that clients can make to an API. When a client exceeds its limit, DRF returns a 429 Too Many Requests response.
Default Behavior
By default, DRF does not apply any throttling. Unless explicitly configured, clients can make an unlimited number of requests to your API.
Setting the Throttling Policy
The most DRY approach to rate limiting is to set a global throttling policy in your settings.py. This ensures your rate limits are consistently applied across all endpoints without needing to repeat code on every view.
You configure global throttling using the DEFAULT_THROTTLE_CLASSES and DEFAULT_THROTTLE_RATES settings:
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/day'
}
}NOTE
All the throttling classes will be evaluated instead of just one. AnonRateThrottle and UserRateThrottle are not mutually exclusive. UserRateThrottle falls back to using the IP address for unauthenticated users, meaning an anonymous client is evaluated by both limits simultaneously.
Rate descriptions used in DEFAULT_THROTTLE_RATES dictate the allowed requests over a given period. The format is number/period, where the period may be second, minute, hour, or day.
Per-View Configuration
If you only want to throttle specific endpoints - or override the global settings for certain views - you can set the throttling policy on a per-view basis.
WARNING
Applying UserRateThrottle or AnonRateThrottle to a specific view does not give that view its own isolated rate limit budget. Throttle counters are global. If 'user': '1000/day' is configured, a user has 1000 requests total across all endpoints using that throttle. If you need isolated budgets for different views, use ScopedRateThrottle.
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.throttling import UserRateThrottle
class ExampleView(APIView):
throttle_classes = [UserRateThrottle] # Overrides global settings
def get(self, request, format=None):
return Response({'status': 'request was permitted'})from rest_framework.decorators import api_view, throttle_classes
from rest_framework.response import Response
from rest_framework.throttling import UserRateThrottle
# Decorators like @throttle_classes must go BELOW @api_view
@api_view(['GET'])
@throttle_classes([UserRateThrottle])
def example_view(request):
return Response({'status': 'request was permitted'})from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.throttling import UserRateThrottle
class ExampleViewSet(viewsets.ViewSet):
throttle_classes = [UserRateThrottle] # Apply to the whole ViewSet
@action(detail=False, methods=['GET'], throttle_classes=[UserRateThrottle])
def limited_action(self, request):
return Response({'status': 'request was permitted'})Disabling Throttling
To exempt a single view from a global throttling policy, set throttle_classes to an empty list:
class HealthCheckView(APIView):
throttle_classes = [] # Never throttled, regardless of global settingsTo switch off a rate everywhere while leaving the throttle class in place, set its scope to None. This is handy for disabling limits in local development without changing your view code:
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_RATES': {
'anon': None, # Anonymous requests are not throttled
'user': '1000/day'
}
}NOTE
A scope set to None is not the same as omitting it. An omitted scope raises ImproperlyConfigured when the throttle runs; None is an explicit "no limit".
Built-in Throttles
DRF provides several built-in throttle classes to handle common use cases:
AnonRateThrottle: Throttles unauthenticated users based on their IP address. This is highly recommended to prevent scraping or brute force attacks on public endpoints (like login views).UserRateThrottle: Throttles all users. It uses the user ID for authenticated users, and falls back to the IP address for unauthenticated users.ScopedRateThrottle: Allows you to define distinct rate limits for different parts of your API, identified by a custom "scope" name.
NOTE
Because AnonRateThrottle relies on IP addresses, many distinct users sharing a corporate network, public Wi-Fi, or cellular network (CGNAT) are lumped into the same "bucket". If one user exhausts the limit, everyone on that IP is blocked.
Using ScopedRateThrottle
When using ScopedRateThrottle, you must define a .throttle_scope attribute on the view. This tells DRF which rate limit to apply from your settings.
WARNING
ScopedRateThrottle fails silently. If the view has no throttle_scope attribute, the throttle is skipped and every request is allowed - no error is raised. Always pair it with a throttle_scope and a matching entry in DEFAULT_THROTTLE_RATES.
from rest_framework.views import APIView
from rest_framework.throttling import ScopedRateThrottle
class ContactListView(APIView):
throttle_classes = [ScopedRateThrottle]
throttle_scope = 'contacts'You configure the specific limits for those scopes in your settings.py:
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.ScopedRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'contacts': '1000/day',
'uploads': '20/day'
}
}For ViewSets, you can apply scopes dynamically by overriding get_throttles():
from rest_framework import viewsets
from rest_framework.throttling import ScopedRateThrottle
class DocumentViewSet(viewsets.ModelViewSet):
throttle_scope = 'uploads'
def get_throttles(self):
if self.action in ['create', 'update']:
return [ScopedRateThrottle()] # Apply uploads limit for writes
return super().get_throttles() # Global limits for readsProtecting Built-In Login Views
DRF's built-in ObtainAuthToken view explicitly sets throttle_classes = () internally. If you route directly to this view, it will ignore your global throttling policy and remain vulnerable to brute-force attacks. To secure it, you must subclass it and explicitly re-apply the throttle:
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.throttling import AnonRateThrottle
class ThrottledLoginView(ObtainAuthToken):
throttle_classes = [AnonRateThrottle]Production Concerns
Throttling that works in development can silently fail or be bypassed in production. Always verify these three configurations:
- Centralized Cache: DRF tracks request counts using Django's cache. In multi-worker environments (Gunicorn/uWSGI), you must use a shared cache backend like Redis or Memcached. The default
LocMemCacheisolates counts per worker, breaking rate limits. - IP Spoofing (
NUM_PROXIES): By default, DRF trusts theX-Forwarded-Forheader, allowing malicious users to bypassAnonRateThrottleby sending fake IPs. You must setNUM_PROXIESinsettings.pyto the exact number of reverse proxies in front of your app (or0if none) so DRF extracts the true client IP securely. - Concurrency Race Conditions: Built-in throttles are not thread-safe under extreme concurrency and may allow requests slightly beyond the limit. For strict enforcement (e.g., billing), use a robust third-party solution.
# settings.py
REST_FRAMEWORK = {
'NUM_PROXIES': 1, # Essential for secure IP identification
}The Throttled Response
When a client exceeds its rate limit, DRF raises a Throttled exception, which is rendered as a 429 Too Many Requests response:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 59
{
"detail": "Request was throttled. Expected available in 59 seconds."
}The Retry-After header tells the client how many seconds to wait before retrying. DRF adds it automatically whenever the throttle's .wait() method returns a value - every built-in throttle implements this, but a custom BaseThrottle subclass must implement it explicitly.
Custom Throttling
Using SimpleRateThrottle
If the built-in throttles mostly meet your needs but you require a custom way to identify users (e.g., rate limiting based on a specific API key, a tenant ID, or a custom user "tier"), you can subclass SimpleRateThrottle.
You only need to override the .get_cache_key() method, which should return a unique string identifying the client to track, or None if the request should not be throttled.
When defining a custom scope, you must also add it to DEFAULT_THROTTLE_RATES in your settings, otherwise DRF will raise an ImproperlyConfigured exception:
# settings.py
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_RATES': {
'premium': '10000/day' # Required for the 'premium' scope below
}
}from rest_framework.throttling import SimpleRateThrottle
class PremiumTierThrottle(SimpleRateThrottle):
scope = 'premium'
def get_cache_key(self, request, view):
if request.user.is_authenticated and request.user.profile.is_premium:
# Throttle premium users separately based on their ID
return self.cache_format % {
'scope': self.scope,
'ident': request.user.pk
}
return None # Do not apply this throttle to non-premium usersUsing BaseThrottle
If you need complete control over the throttling logic - perhaps integrating with an external rate-limiting service or using an alternative storage mechanism - you can subclass BaseThrottle and implement the following methods:
.allow_request(self, request, view): Must returnTrueif the request should be allowed, andFalseotherwise..wait(self): Optional. If implemented, it should return the recommended number of seconds to wait before attempting the next request. This is only called if.allow_request()returnsFalse. If implemented, DRF will automatically include aRetry-Afterheader in the HTTP response.
import random
from rest_framework.throttling import BaseThrottle
class RandomThrottle(BaseThrottle):
def allow_request(self, request, view):
# Allow 90% of requests through randomly
return random.randint(1, 10) != 1Burst and Sustained Rates (Advanced Pattern)
The most robust throttling setup uses two limits simultaneously: a "burst" rate (to prevent rapid spikes) and a "sustained" rate (to enforce a longer-term budget). You can achieve this by applying multiple throttles at once.
To prevent your throttle counters from being evicted when your main cache fills up, it's a best practice to assign throttles to a dedicated cache backend using the cache attribute.
# settings.py
CACHES = {
'default': { ... },
'throttling': { ... } # Dedicated Redis DB or Memcached instance
}
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_RATES': {
'burst': '60/minute',
'sustained': '1000/day'
}
}from rest_framework.throttling import UserRateThrottle
from django.core.cache import caches
class BurstRateThrottle(UserRateThrottle):
scope = 'burst'
cache = caches['throttling']
class SustainedRateThrottle(UserRateThrottle):
scope = 'sustained'
cache = caches['throttling']You can then apply both throttles globally (via DEFAULT_THROTTLE_CLASSES) or per-view:
# settings.py
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'my_app.throttling.BurstRateThrottle',
'my_app.throttling.SustainedRateThrottle'
],
# ...
}# views.py
class ExampleView(APIView):
throttle_classes = [BurstRateThrottle, SustainedRateThrottle]TIP
If you need more granular rate limiting (e.g., throttling individual functions or based on complex request data outside of DRF), consider using standard third-party packages like django-ratelimit rather than building complex throttling systems from scratch.
