When building a web application or an API with Django, use this link early development is often characterized by small datasets. Your database has a dozen records, queries return instantly, and everything feels lightning fast. But as your application grows to tens of thousands—or millions—of rows, reality sets in.
Loading an entire database table into memory to render a template or return a JSON response will rapidly choke your server, spike memory usage, and cause request timeouts.
The antidote to this performance bottleneck is Pagination.
In this comprehensive guide, we will explore how pagination works in both traditional Django template-based views and modern Django REST Framework (DRF) APIs, examine built-in pagination styles, and review best practices for high-performance applications.
Why Pagination is Non-Negotiable
Pagination is the practice of dividing a large dataset into discrete, manageable chunks (or “pages”) delivered to the client on demand. Without it, you face three major risks:
- Memory Exhaustion: Fetching thousands of ORM objects and serializing them simultaneously consumes massive RAM on your application server.
- Network Latency: Transferring megabytes of JSON data over the network degrades client-side performance, especially on mobile devices with constrained bandwidth.
- Database Strain: Large SQL queries without limits or offsets force database engines to scan entire tables, locking resources and destroying query efficiency.
Fortunately, Django provides powerful tools out of the box to solve this at every level of your stack.
Part 1: Pagination in Vanilla Django (Template-Based Views)
For server-side rendered applications using Django templates, Django provides a built-in Paginator class inside django.core.paginator.
1. Implementing the Paginator in a View
Imagine a blog application where we want to display 10 articles per page. Here is how you implement the paginator in a standard function-based view:
Python
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.shortcuts import render
from .models import Article
def article_list_view(request):
article_list = Article.objects.all().order_by('-created_at')
# Show 10 articles per page
paginator = Paginator(article_list, 10)
# Get the requested page number from URL query params (e.g., ?page=2)
page_number = request.GET.get('page')
try:
articles = paginator.get_page(page_number)
except PageNotAnInteger:
# If page is not an integer, deliver the first page
articles = paginator.page(1)
except EmptyPage:
# If page is out of range (e.g. page 9999), deliver last page of results
articles = paginator.page(paginator.num_pages)
return render(request, 'articles/article_list.html', {'articles': articles})
Note: Using paginator.get_page(page_number) is generally preferred over paginator.page() because it automatically handles invalid or out-of-bounds page numbers gracefully.
2. Rendering Pagination Links in the Template
Once the Page object is passed to your template, you can easily build navigation controls:
HTML
<div class="pagination">
<span class="step-links">
{% if articles.has_previous %}
<a href="?page=1">« first</a>
<a href="?page={{ articles.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ articles.number }} of {{ articles.paginator.num_pages }}.
</span>
{% if articles.has_next %}
<a href="?page={{ articles.next_page_number }}">next</a>
<a href="?page={{ articles.paginator.num_pages }}">last »</a>
{% endif %}
</span>
</div>
Part 2: Pagination in Django REST Framework (DRF)
When building APIs for frontend frameworks (like React, browse around these guys Vue, or mobile apps), pagination needs to be serialized into clean JSON structures containing metadata (like count, next, and previous URLs).
DRF provides three primary built-in pagination classes.
1. PageNumberPagination (The Standard Approach)
This is the most common pagination style, mirroring traditional web pagination. Clients request data by page number and page size.
Global Configuration (settings.py):
Python
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20, # Default number of items per page
}
Customizing Page Size Per View:
If a specific endpoint needs a different page size, you can create a custom pagination subclass:
Python
from rest_framework.pagination import PageNumberPagination
class StandardResultsSetPagination(PageNumberPagination):
page_size = 20
page_size_query_param = 'page_size' # Allows client to pass ?page_size=50
max_page_size = 100 # Prevents clients from requesting 10,000 items at once
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
pagination_class = StandardResultsSetPagination
When a client queries /api/articles/?page=2, DRF returns a structured JSON response:
JSON
{
"count": 145,
"next": "http://api.example.com/api/articles/?page=3",
"previous": "http://api.example.com/api/articles/?page=1",
"results": [
{
"id": 21,
"title": "Understanding Django ViewSets",
"author": "Jane Doe"
}
// ... 19 more items
]
}
2. LimitOffsetPagination (The SQL-Style Approach)
Inspired by SQL LIMIT and OFFSET clauses, this style allows clients to specify exactly where to start slicing the dataset and how many items to return.
limit: The maximum number of items to return (e.g.,?limit=20).offset: The starting index from which to begin the slice (e.g.,offset=40starts at the 41st item).
Python
from rest_framework.pagination import LimitOffsetPagination
class ArticleLimitOffsetPagination(LimitOffsetPagination):
default_limit = 20
max_limit = 100
This style is particularly useful for client-side applications that implement “load more” buttons or custom slider components where absolute page numbers matter less than contiguous blocks of data.
3. CursorPagination (The High-Performance Infinite Scroll)
Standard page-number and offset pagination suffer from a major performance flaw at scale: deep pagination penalty.
When a database executes an OFFSET 100000 LIMIT 20 query, it still has to read and discard the first 100,000 rows before returning the 20 you requested. As your table grows, deep pages become exponentially slower.
CursorPagination solves this by using an opaque, encoded string (a cursor) pointing to a specific database record based on an ordered field (like timestamp or ID).
Python
from rest_framework.pagination import CursorPagination
class ArticleCursorPagination(CursorPagination):
page_size = 20
ordering = '-created_at' # Must be a unique or indexed field
cursor_query_param = 'cursor'
Why Choose Cursor Pagination?
- Constant Performance: Query execution time remains lightning-fast whether you are on page 2 or page 10,000.
- No Duplicate/Missing Records: If new rows are inserted while a user is scrolling through an infinite feed, traditional page numbers shift and can cause items to repeat or disappear. Cursor pagination stays locked to the pointer.
Summary Comparison: Choosing the Right Strategy
| Pagination Class | Best Used For | Client Query Example | Performance at Scale |
| PageNumberPagination | Standard web apps, admin panels, predictable navigation. | ?page=2&page_size=20 | Degrades on extremely deep pages ($>10,000$ pages). |
| LimitOffsetPagination | Custom desktop/mobile UI sliders, infinite scroll with explicit jumps. | ?limit=20&offset=40 | Degrades heavily with large offsets. |
| CursorPagination | High-throughput feeds, real-time logs, infinite scrolling. | ?cursor=cD0yMDI2... | Extremely fast and constant regardless of depth. |
Production Best Practices for Pagination
- Always Set a Maximum Page Size: Never leave
max_page_sizeunrestricted. Malicious or accidental queries requesting?page_size=100000can instantly crash your database server. - Ensure Proper Database Indexing: If you are sorting or filtering by specific fields (like
created_atorprice), ensure those columns have database indexes (db_index=True) to prevent full table scans during pagination. - Combine with Select Related: When serializing related foreign key objects across paginated results, always optimize your queryset with
.select_related()and.prefetch_related()to eliminate $N+1$ query issues.
Conclusion
Pagination is a foundational pillar of building scalable, production-ready Django applications. Whether you are rendering server-side templates with Django’s built-in Paginator or serving high-speed JSON feeds using Django REST click here for more info Framework’s cursor and page-number engines, understanding how to control data flow protects your server resources and delivers a seamless user experience.