Skip to content

DRF Production Checklist

Prerequisites

Django's Production Checklist

Authentication

In production, use only SimpleJWT. In test environments, add SessionAuthentication alongside it to support the Admin UI, Browsable API, and API documentation. Use drf-spectacular for your API documentation. In production, protect the schema by serving it using SERVE_AUTHENTICATION and SERVE_PERMISSIONS.

Remove LoginRequiredMiddleware for DRF because it redirects unauthenticated requests to a login page. Instead, set DEFAULT_PERMISSION_CLASSES to IsAuthenticated. If you use Apache mod_wsgi, ensure you set the WSGIPassAuthorization On directive. Otherwise, the Authorization header will be stripped before it reaches Django.

python
# production
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
    ],
}
SPECTACULAR_SETTINGS = {
    'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAdminUser'],
    'SERVE_AUTHENTICATION': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

# test
# test
if DEBUG:
    REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'].append(
        'rest_framework.authentication.SessionAuthentication'
    )
    REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'].append(
        'rest_framework.renderers.BrowsableAPIRenderer'
    )
    SPECTACULAR_SETTINGS['SERVE_AUTHENTICATION'] = [
        'rest_framework.authentication.SessionAuthentication',
    ]

Permissions

Set IsAuthenticated as DEFAULT_PERMISSION_CLASSES globally so endpoints are closed by default, and use custom permission classes at view level for object-level checks. Note that has_object_permission only runs when a view calls get_object(), so it never fires on list actions — scope the queryset in get_queryset() too, which for list endpoints is the only enforcement there is. Views that need public access must opt out explicitly with permission_classes = [AllowAny], which replaces the global default rather than adding to it.

python
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ]
}

Filtering

Always scope the queryset to what the requester is allowed to see by overriding get_queryset. Register DjangoFilterBackend and SearchFilter globally, then set filterset_fields and search_fields on the specific views that need them. If you need to use OrderingFilter, apply it directly to the view instead of globally and define your ordering_fields.

Pagination

Use PageNumberPagination with PAGE_SIZE set to around 100, but lower this number for endpoints with heavily nested serializers. Switch to CursorPagination for large or frequently updated tables where per-request COUNT(*) and deep offsets become expensive.

Make sure every list endpoint's queryset has an explicitly defined ordering that includes a unique tiebreaker, such as id, to resolve ties when rows share the same values.

Caching

Caching is fine for responses that are identical for every user. You can apply this by wrapping the view with cache_page in your urls.py file. However, be careful with private or per-user responses. The cache_page decorator caches based on the URL and not the user, so two people visiting the same endpoint can share an entry and see each other's data. Avoid caching these responses unless you build a custom, user-specific cache key yourself.

Versioning

Although the DRF docs lean toward AcceptHeaderVersioning, it is better to stick with URLPathVersioning. This is mainly due to caching issues with accept-header versioning, the overhead of combining it with drf-spectacular, and the loss of discoverability for third-party consumers, who have no versioned URL to paste into a browser or a ticket.

Throttling

Extend the AnonRateThrottle and UserRateThrottle throttling classes as shown below, and register them globally:

python
from rest_framework.throttling import AnonRateThrottle, UserRateThrottle
from django.core.cache import caches

class AuthenticatedOnlyMixin:
    """Skip a throttle entirely for anonymous requests."""
    def get_cache_key(self, request, view):
        if not request.user.is_authenticated:
            return None
        return super().get_cache_key(request, view)

class AnonBurstRateThrottle(AnonRateThrottle):
    scope = 'anon-burst'
    cache = caches['throttling']

class AnonSustainedRateThrottle(AnonRateThrottle):
    scope = 'anon-sustained'
    cache = caches['throttling']

class UserBurstRateThrottle(AuthenticatedOnlyMixin, UserRateThrottle):
    scope = 'user-burst'
    cache = caches['throttling']

class UserSustainedRateThrottle(AuthenticatedOnlyMixin, UserRateThrottle):
    scope = 'user-sustained'
    cache = caches['throttling']
python
# settings.py
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'my_app.throttling.AnonBurstRateThrottle',
        'my_app.throttling.AnonSustainedRateThrottle',
        'my_app.throttling.UserBurstRateThrottle',
        'my_app.throttling.UserSustainedRateThrottle'
    ],
    # ...
}
  • Next, define your throttle rates as below:
python
'DEFAULT_THROTTLE_RATES': {
    'anon-burst': '60/minute',
    'anon-sustained': '1000/day',
    'user-burst': '100/minute',
    'user-sustained': '10000/day',
}
  • Make sure you use a separate cache for throttling.
python
# settings.py
CACHES = {
    'default': { ... },
    'throttling': { ... } # Dedicated Redis DB or Memcached instance
}
  • Set NUM_PROXIES = 0 when there's no proxy, and set it to the exact number of proxies in front of your application when there is.
  • Improve Throttle errors experience on frontend by displaying Retry-After header value in the error messages.