Skip to main content

Command Palette

Search for a command to run...

How to Optimize Django REST Framework APIs in 2026

Updated
9 min read
D
Content Strategist/Software developer

Django REST Framework is a popular option for Python developers. It comes with proper documentation that is easy to understand, giving developers the capacity to build working APIs quickly and effectively. However, building a working API is not enough. The best APIs are fast in addition to working effectively. The default Django REST Framework setup may need to be optimized once your app starts handling huge traffic. This article discusses a few practical methods developers can use to optimize a Django REST API in 2026. It covers database query tuning, response caching, and async support.

Stop the N+1 Problem Before It Stops You

The most common performance issue for most Django APIs today is the N+1 query problem. This issue arises when the serializer fetches a related object for all queryset items, leading to an extra database query for every row.

Here is an example setup:


# models.py
class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
python
# serializers.py
class BookSerializer(serializers.ModelSerializer):
    author_name = serializers.CharField(source='author.name')

    class Meta:
        model = Book
        fields = ['id', 'title', 'author_name']
python
# views.py — THE PROBLEM
class BookListView(generics.ListAPIView):
    serializer_class = BookSerializer
    queryset = Book.objects.all()  # No select_related — fires 1 + N queries

In this instance, 101 queries are fired for the 100 books. An easy fix is with select_related for ForeignKey/OneToOne and prefetch_related for ManytoMany:

# views.py — THE FIX
class BookListView(generics.ListAPIView):
    serializer_class = BookSerializer
    queryset = Book.objects.select_related('author').all()
For reverse relations or M2M, use prefetch_related:
python
class AuthorListView(generics.ListAPIView):
    serializer_class = AuthorSerializer
    queryset = Author.objects.prefetch_related('book_set').all()

To quickly catch N+1 problems in early development stages, use django-debug-toolbar  or consider logging SQL queries.

Use only() and defer() to Limit DB Column Fetching

When your model includes 20 fields but your serializer only exposes less than 10, you will be using extra unnecessary resources to fetch data from the database on every API request. This happens because, by default, Django fetches all columns.

# Fetch only what the serializer actually needs
class BookListView(generics.ListAPIView):
    serializer_class = BookSerializer
    queryset = Book.objects.select_related('author').only(
        'id', 'title', 'author__name'
    )

Instead of whitelisting specific fields like binary fields and large text, consider using defer().


queryset = Book.objects.defer('full_content', 'cover_image_data')

This will come in handy when you’re dealing with large columns of TextField or BinaryField  that are rarely used in list endpoints.

Paginate Everything — and Tune It

Fetching unbounded querysets can be tricky and complex for most APIs. Django REST Framework APIs ships with three paginator classes: PageNumberPagination, LimitOffsetPagination, and CursorPagination. For most cases, CursorPagination is the most performant at scale because it doesn't require a COUNT(*) query.

# pagination.py
from rest_framework.pagination import CursorPagination

class BookCursorPagination(CursorPagination):
    page_size = 20
    ordering = '-created_at'
    page_size_query_param = 'page_size'
    max_page_size = 100
python
# views.py
class BookListView(generics.ListAPIView):
    serializer_class = BookSerializer
    pagination_class = BookCursorPagination
    queryset = Book.objects.select_related('author').only('id', 'title', 'author__name')

Set your global defaults in settings.py so you never accidentally return a full table

settings.py

REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 20, }

Serialize Smarter with Serializer Optimization

Serializers are a major cause of increased CPU time for many API queries. These strategies are effective for optimizing serializers and developing better Django REST APIs. When possible, use read_only=True since DRF APIs save time and resources by skipping validation logic for read-only fields when deserializing.

Have list endpoints with no nested serializers. Even though nested serializers are convenient, they often add overhead that is unnecessary. For simple lookups, use SerializerMethodField or StringRelatedField

class BookSerializer(serializers.ModelSerializer):
    # Instead of a full nested AuthorSerializer:
    author_name = serializers.StringRelatedField(source='author')

    class Meta:
        model = Book
        fields = ['id', 'title', 'author_name']
        read_only_fields = ['id']

Avoid using numerous SerializerMethodField calls but instead use to_representation for conditional field logic:

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'internal_notes']

    def to_representation(self, instance):
        data = super().to_representation(instance)
        # Only include internal_notes for staff users
        request = self.context.get('request')
        if not request or not request.user.is_staff:
            data.pop('internal_notes', None)
        return data

Cache Aggressively with Django's Cache Framework

Caching is a reliable method for optimizing Django REST Framework APIs and Redis is the mot popular cache backend in web development. Redis caching setting up:

# settings.py
CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        },
        "TIMEOUT": 300,  # 5 minutes default
    }
}

For DRF views, you can cache entire responses using cache_page:

from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page

class BookListView(generics.ListAPIView):
    serializer_class = BookSerializer
    queryset = Book.objects.select_related('author')

    @method_decorator(cache_page(60 * 15))  # Cache for 15 minutes
    def get(self, *args, **kwargs):
        return super().get(*args, **kwargs)

For more granular control, cache at the queryset level:

from django.core.cache import cache

class BookListView(generics.ListAPIView):
    serializer_class = BookSerializer

    def get_queryset(self):
        cache_key = 'book_list_all'
        books = cache.get(cache_key)
        if books is None:
            books = list(
                Book.objects.select_related('author').only('id', 'title', 'author__name')
            )
            cache.set(cache_key, books, timeout=300)
        return books

Every time data changes, ensure you invalidate caches especially since a stale cache is more harmful that having no cache at all.

Use Database Indexes Strategically

When an application contains a missing index, there is no known optimization technique that can improve its performance. Ensure you add a database index for every fields that you filter, order, or join frequently.

class Book(models.Model):
    title = models.CharField(max_length=200, db_index=True)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    published_at = models.DateTimeField()
    genre = models.CharField(max_length=50)

    class Meta:
        indexes = [
            models.Index(fields=['published_at', 'genre']),  # Composite index
            models.Index(fields=['-published_at']),           # Descending index
        ]

Run EXPLAIN ANALYZE (PostgreSQL) or EXPLAIN (MySQL) on your slow queries to verify indexes are being used:

EXPLAIN ANALYZE
SELECT * FROM api_book
WHERE genre = 'fiction'
ORDER BY published_at DESC
LIMIT 20;

Your index might be missing whenever you see Seq Scan where you expect an Index Scan.

Async Views with Django 4.x+ and ASGI

Async view support has been a feature of Django since Django version 3.1. These day, DRF style views are an option for some I/O- bound endpoints.

For truly async views you'll want to look at djangorestframework with ASGI, or use adrf (Async DRF):

pip install adrf
python
# views.py with adrf
import adrf.views as async_views
import asyncio
import httpx

class ExternalDataView(async_views.APIView):
    async def get(self, request):
        async with httpx.AsyncClient() as client:
            # Fire multiple external requests concurrently
            response1, response2 = await asyncio.gather(
                client.get('https://api.example.com/data1'),
                client.get('https://api.example.com/data2'),
            )

        return Response({
            'data1': response1.json(),
            'data2': response2.json(),
        })

Django ORM is still synchronous. For async database access, use sync_to_async:

from asgiref.sync import sync_to_async

class AsyncBookListView(async_views.APIView):
    async def get(self, request):
        books = await sync_to_async(list)(
            Book.objects.select_related('author').only('id', 'title')
        )
        serializer = BookSerializer(books, many=True)
        return Response(serializer.data)

Async does not work for endpoints that are compute-bound. Developers are encouraged to stick to synchronous views in such circumstances.

Throttling and Rate Limiting to Protect Your API

Optimizing your Django REST APIs for performance involves more than making them faster. It includes identifying and preventing performance issues way before they manifest. Features such as rate limiting and throttling are easy to setup and protect your API from misuse. Here is an example:

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

To finetune control further, custom throttle classes will do the trick:

# throttles.py
from rest_framework.throttling import UserRateThrottle

class BurstRateThrottle(UserRateThrottle):
    scope = 'burst'

class SustainedRateThrottle(UserRateThrottle):
    scope = 'sustained'
python
# settings.py
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'myapp.throttles.BurstRateThrottle',
        'myapp.throttles.SustainedRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'burst': '60/min',
        'sustained': '1000/day',
    }

Use values() and values_list() for Read-Only Endpoints

You will be able to skip model instantiation completely when using values()and values_list()to fetch tuples from a database directly. This is especially needed when you’re developing a read-only endpoint and do not require full model instances.

from rest_framework.views import APIView
from rest_framework.response import Response

class BookTitleListView(APIView):
    def get(self, request):
        # Returns list of dicts — much faster than full model instantiation
        books = list(
            Book.objects.values('id', 'title', 'author__name')
                        .order_by('-published_at')[:50]
        )
        return Response(books)

However, it comes with a tradeoff; you lose the methods and properties.

Profile Before You Optimize

Optimizing an application blindly without having a thorough audit is a common mistake that developers make when creating Django REST APIs. There are many effective profiling tool that you can use to measure your applications performance before making optimization changes:

bash
pip install django-silk
python
# settings.py
INSTALLED_APPS += ['silk']
MIDDLEWARE += ['silk.middleware.SilkyMiddleware']

# urls.py
urlpatterns += [path('silk/', include('silk.urls', namespace='silk'))]

django-debug-toolbar for SQL query inspection:

bash
pip install django-debug-toolbar
python
# settings.py (development only)
if DEBUG:
    INSTALLED_APPS += ['debug_toolbar']
    MIDDLEWARE.insert(0, 'debug_toolbar.middleware.DebugToolbarMiddleware')
    INTERNAL_IPS = ['127.0.0.1']

For production profiling, use py-spy for low-overhead sampling:

bash
pip install py-spy
py-spy top --pid <your_gunicorn_pid>

Putting It All Together

When you implement all these optimization techniques for your application, it will look like this:

# models.py
class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey('Author', on_delete=models.CASCADE)
    genre = models.CharField(max_length=50, db_index=True)
    published_at = models.DateTimeField()

    class Meta:
        indexes = [
            models.Index(fields=['genre', '-published_at']),
        ]


# serializers.py
class BookSerializer(serializers.ModelSerializer):
    author_name = serializers.CharField(source='author.name', read_only=True)

    class Meta:
        model = Book
        fields = ['id', 'title', 'author_name', 'genre', 'published_at']
        read_only_fields = ['id', 'published_at']


# pagination.py
class BookCursorPagination(CursorPagination):
    page_size = 25
    ordering = '-published_at'
    max_page_size = 100


# views.py
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page

class OptimizedBookListView(generics.ListAPIView):
    serializer_class = BookSerializer
    pagination_class = BookCursorPagination

    def get_queryset(self):
        qs = Book.objects.select_related('author').only(
            'id', 'title', 'genre', 'published_at', 'author__name'
        )
        genre = self.request.query_params.get('genre')
        if genre:
            qs = qs.filter(genre=genre)
        return qs

    @method_decorator(cache_page(60 * 5))
    def get(self, *args, **kwargs):
        return super().get(*args, **kwargs)

Final Thoughts

Django REST Framework APIs need to be consistently monitored and optimized for performance through a layered and systematic process. Developers can start with simple techniques such as database query optimization before moving on to other techniques like pagination, caching, and serializer tuning. Remember to utilize Django profiling tools when making strategic and informed optimization decisions.

Most common performance issues in DRF APIs can be fixed without the need for architectural changes. A well-optimized DRF application has the capacity to handle thousands of requests per second, even on average hardware. Developers need to understand where they spend the most time in optimization efforts and then make long-lasting changes.