DRF Throttling

Protect your API from abuse and runaway clients with rate limiting — anonymous, per-user and scoped.

Throttling answers ‘how often may this client call me?’ It is your first line of defence against scrapers, brute-force login attempts and a buggy client stuck in a retry loop. DRF has a clean built-in system driven by settings.

The three built-in classes

  • AnonRateThrottle — limits unauthenticated clients by IP.
  • UserRateThrottle — limits authenticated users by account.
  • ScopedRateThrottle — different limits for different endpoints via a throttle_scope.
# settings.py
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/day',
        'user': '1000/day',
    },
}

The rate string is number/period where period is second, minute, hour or day. When exceeded, DRF returns 429 Too Many Requests with a Retry-After header.

DRF throttling is backed by the cache. On more than one server, point the cache at a shared Redis — a per-process LocMemCache means each worker counts separately and your real limit becomes N× what you configured.

Example

Example · python
from rest_framework.throttling import ScopedRateThrottle, UserRateThrottle
from rest_framework import viewsets


# A custom throttle keyed on the user's plan, not a flat rate.
class BurstRateThrottle(UserRateThrottle):
    scope = 'burst'

class SustainedRateThrottle(UserRateThrottle):
    scope = 'sustained'


class ReportViewSet(viewsets.ModelViewSet):
    queryset = Report.objects.all()
    serializer_class = ReportSerializer
    # Combine two windows: short burst + long sustained ceiling.
    throttle_classes = [BurstRateThrottle, SustainedRateThrottle]

    def get_throttles(self):
        # The expensive export action gets its own, tighter scope.
        if self.action == 'export':
            self.throttle_scope = 'exports'
            return [ScopedRateThrottle()]
        return super().get_throttles()


# settings.py
# REST_FRAMEWORK = {
#     'DEFAULT_THROTTLE_RATES': {
#         'burst': '60/min',       # no more than 60 requests a minute
#         'sustained': '1000/day', # and no more than 1000 a day
#         'exports': '5/hour',     # the heavy endpoint is rationed hard
#     },
# }
#
# CACHES = {'default': {
#     'BACKEND': 'django.core.cache.backends.redis.RedisCache',
#     'LOCATION': 'redis://cache:6379/1',   # shared, so counts are global
# }}

When to use it

  • A developer applies AnonRateThrottle to limit unauthenticated API requests to 100 per day to deter scraping.
  • A team uses UserRateThrottle to cap authenticated requests at 1000/hour to protect against abusive clients.
  • A developer writes a custom throttle class to rate-limit POST requests on a registration endpoint to 5 per hour.

More examples

Global throttle settings

Configures global rate limits: 100 requests/day for anonymous users and 1000/hour for authenticated ones.

Example · python
# settings.py
REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.AnonRateThrottle",
        "rest_framework.throttling.UserRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {
        "anon": "100/day",
        "user": "1000/hour",
    },
}

Per-view throttle override

Applies a stricter 10/min burst throttle specifically on the login endpoint to limit brute-force attempts.

Example · python
from rest_framework.throttling import UserRateThrottle

class BurstRateThrottle(UserRateThrottle):
    scope = "burst"
    # settings: 'burst': '10/min'

class LoginView(APIView):
    throttle_classes = [BurstRateThrottle]

    def post(self, request):
        ...

Custom throttle for registration

A custom throttle class that limits account registrations to 5 per hour per IP address.

Example · python
from rest_framework.throttling import SimpleRateThrottle

class RegistrationThrottle(SimpleRateThrottle):
    scope = "registration"
    # settings: 'registration': '5/hour'

    def get_cache_key(self, request, view):
        return self.cache_format % {
            "scope": self.scope,
            "ident": self.get_ident(request),
        }

Discussion

  • Be the first to comment on this lesson.