Taming DRF List Views: Pagination Limits, Prefetching, and Tight Serializers
List endpoints that are fast with ten rows become slow in production. Cap page size, prefetch related data in get_queryset, and whitelist serializer fields to keep responses lean and predictable.
18 Jan 2026, 00:21 UTC

List endpoints feel fast in development with ten rows and then stall in production. The useful takeaway is to treat a DRF list view as three decisions: how many rows you allow per request, how you load related rows, and which fields you serialize. Get those right and N+1 queries and payload bloat disappear without rewriting the view.
Why list views degrade: N+1 and unbounded pages
An N+1 query happens when the serializer accesses a related object for each item in the list, triggering one extra database query per row. With a foreign key like book.author, iterating 100 books can produce 101 queries instead of one join.
Pagination also matters. PageNumberPagination respects a page_size query parameter but defaults to a class-defined limit. Without a max_page_size a client can request thousands of items, increasing response time and memory use on the application server.
Cap the page, then shape the queryset
Pagination guardrails
Define a pagination class and attach it to the list view. page_size sets the default, page_size_query_param allows callers to request a smaller page, and max_page_size caps the upper bound.
from rest_framework.pagination import PageNumberPagination
class StandardResultsSetPagination(PageNumberPagination):
page_size = 20
page_size_query_param = 'page_size'
max_page_size = 100
Risk: setting max_page_size too high exposes the endpoint to large responses. Keep the cap aligned with your read replica capacity and client expectations.
Prefetch what the serializer actually uses
Move loading logic into get_queryset. select_related performs a SQL join for ForeignKey and OneToOne relations, reducing lookups to a single query. prefetch_related issues a separate query and joins in Python for ManyToMany and reverse ForeignKey relations.
from rest_framework import generics
class BookListView(generics.ListAPIView):
serializer_class = BookSerializer
pagination_class = StandardResultsSetPagination
def get_queryset(self):
return Book.objects.select_related('author','publisher').prefetch_related('tags')
select_related only works for forward FK/OneToOne. Using it on a ManyToMany relation has no effect. Prefetching adds an extra query but avoids per-row queries.
Keep serializers explicit, not deep
Serializer depth=1 auto-nests one level of related fields. That is convenient but can bloat responses and inadvertently include sensitive related fields not intended for public consumption.
Explicit fields with read_only sources give a stable contract and smaller payloads.
class BookSerializer(serializers.ModelSerializer):
author_name = serializers.ReadOnlyField(source='author.name')
publisher_name = serializers.ReadOnlyField(source='publisher.name')
class Meta:
model = Book
fields = ['id','title','author_name','publisher_name','published_date']
Trade-offs and limits
select_related increases the size of the initial join. For wide tables or many relations the join can become heavier than two smaller queries. Prefetch_related always adds at least one extra query.
max_page_size is enforced by DRF, but clients can still request the maximum on every call. Combine pagination with caching or rate limiting for expensive lists.
Practical check
Enable Django DEBUG mode in a non-production environment and inspect query count for a list request. In a Django shell or a temporary debug view you can inspect django.db.connection.queries after a request. With select_related and prefetch_related in place the query count should be stable regardless of page size, whereas without them it grows with the number of items.
Append ?page_size=50 to the endpoint and compare response size and timing with the default. Verify that requests above max_page_size are capped by DRF.
Audit each list endpoint for three things: a pagination class with a sensible max_page_size, a get_queryset that prefetches only the relations used by the serializer, and a serializer that declares an explicit field list. That is the minimal, repeatable pattern for predictable DRF list performance.