Django REST Framework performance: a practical checklist
Most slow Django APIs aren't slow because of Python. They're slow because of the database. This is the order I work through when an endpoint needs to get faster.
0. Measure before you touch anything
Guessing wastes time. Find out which endpoint is slow and why before changing code.
- django-debug-toolbar or django-silk in development show every SQL query an endpoint runs and how long each took.
- Look at the query count first. A list endpoint running 1 query per row is the most common problem by far.
- Write a test that pins the query count, so the problem can't quietly come back:
def test_order_list_query_count(self):
with self.assertNumQueries(3):
self.client.get("/api/orders/")
1. Kill N+1 queries
A serializer that touches a related object triggers one extra query per row. With 50 orders, that's 51 queries instead of 1.
class OrderSerializer(serializers.ModelSerializer):
customer_name = serializers.CharField(source="customer.name")
items = OrderItemSerializer(many=True)
class Meta:
model = Order
fields = ["id", "customer_name", "items", "total"]
Fix it in the view's queryset:
class OrderViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = OrderSerializer
def get_queryset(self):
return (
Order.objects
.select_related("customer") # ForeignKey / OneToOne: SQL JOIN
.prefetch_related("items__product") # reverse FK / M2M: one extra query
)
select_relatedfor forward ForeignKey and OneToOne relations.prefetch_relatedfor reverse relations and ManyToMany. Use aPrefetch()object when you need to filter or order the related rows.
2. Only fetch what you send
If the list view shows five fields, don't load a model with a large text column for every row.
Product.objects.only("id", "name", "price", "thumbnail")
Compute counts and totals in the database with annotate() instead of in a Python loop or a SerializerMethodField that runs a query per row:
from django.db.models import Count
Vendor.objects.annotate(product_count=Count("products"))
3. Add the right indexes
Any field you filter, order or search by on a large table is a candidate for an index. Check the plan with queryset.explain() and look for sequential scans on big tables.
class Order(models.Model):
# ...
class Meta:
indexes = [models.Index(fields=["customer", "-created_at"])]
4. Always paginate
An unpaginated list endpoint gets slower every day your product grows. Set a default in settings:
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
}
For very large or fast-changing tables, CursorPagination avoids expensive COUNT(*) and OFFSET queries.
5. Cache what rarely changes
Category lists, settings and public catalog pages are read thousands of times and change rarely. Cache them in Redis:
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
@method_decorator(cache_page(60 * 5), name="list")
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
...
Only cache responses that are the same for every user, or include the user in the cache key. Serving one user's data to another is far worse than a slow page.
6. Move slow work out of the request
Sending emails, generating PDFs, calling a slow third-party API or resizing images shouldn't block the response. Hand it to a background worker with Celery and a broker such as Redis or RabbitMQ, then return immediately.
@shared_task
def send_order_confirmation(order_id):
...
# in the view
send_order_confirmation.delay(order.id)
7. Then look at the server
Only after the database work is done does it make sense to tune the server: enough Gunicorn workers for your CPU count, persistent DB connections (CONN_MAX_AGE), gzip and a CDN for static and media files.
The short version
- Measure query count and time per endpoint.
- Fix N+1 with
select_relatedandprefetch_related. - Fetch fewer columns and aggregate in SQL.
- Index what you filter and sort on.
- Paginate every list.
- Cache shared, rarely changing data.
- Push slow work to Celery.
On my internship work at Bridgeon I delivered around a 20% backend speed-up on REST APIs built with Django. Most of that kind of gain comes from steps 1 to 4. They're unglamorous, but they compound.
Is your Django app slow?
I audit and speed up Django and DRF backends as a freelancer. Send me the slow endpoint and I'll tell you where the time goes.