Choosing Django Paginator vs. DRF Pagination: A Decision Guide
A concise guide to choosing between Django’s low‑level Paginator and DRF’s PageNumberPagination or CursorPagination. Compare performance, use cases, and see a step‑by‑step implementation to validate your decision.
21 Nov 2025, 05:38 UTC

Problem & Decision Point
When your Django app needs to display or expose large result sets, you must decide how to split the data into pages. Django ships a low‑level Paginator class that works with any list or QuerySet, while Django REST Framework (DRF) provides higher‑level pagination classes like PageNumberPagination and CursorPagination. The choice affects performance, user experience, and API stability.
Decision Constraints
- Data size:
QuerySetwith < 10k rows vs. >100k rows. - Navigation model: Users need to jump to arbitrary pages or only scroll forward.
- Consistency requirement: Avoid duplicate or missing items when the underlying table changes.
- API consumer: Web UI, mobile app, or third‑party integration.
- Database load tolerance: Acceptable use of
OFFSETvs. constant‑time queries.
Supported Options
| Pagination Type | Where to Use | Key Feature | Performance | Limitations |
|---|---|---|---|---|
| django.core.paginator.Paginator | Admin panels, simple list views | Offset‑based page navigation | O(n) for large offsets; fine for <10k rows | Duplicate/missing items on concurrent modifications; no cursor support |
| DRF PageNumberPagination | REST APIs with page numbers | Same offset logic, but integrated with DRF response format | Same as Paginator; can be cached | Same offset drawbacks; requires page number in query string |
| DRF CursorPagination | Infinite scroll, mobile feeds | Cursor based on a unique, sortable field (e.g., id) | Constant‑time O(1) regardless of dataset size | No direct page number access; requires strict ordering |
Trade‑Off Analysis
- Offset vs. Cursor: Offset queries generate
LIMIT … OFFSET …which forces the database to walk through rows before reaching the target page. For tables with millions of rows, this can lock the index and increase latency. Cursor pagination uses aWHERE id > last_seen_idstyle query, keeping the index seek constant. - Jumping to a page: Administrators often need to jump to page 42. Paginator and PageNumberPagination support this out of the box. CursorPagination cannot, so you’d need custom logic.
- Data consistency: If new rows are inserted while a user paginates, offset paginators may skip or duplicate items. CursorPagination, by anchoring to a stable key, preserves order and avoids duplicates.
- Implementation complexity: Paginator is trivial to use in a class‑based view. DRF paginators require configuring the viewset or globally in settings.
- Testing overhead: To verify performance, you can run a quick benchmark using Django’s test client and
django.db.connection.queriesto inspect the generated SQL.
Concrete Implementation & Validation
1. Using Paginator in a Django View
Typical use case: an admin list page where the user can jump to any page number.
# views.py
from django.core.paginator import Paginator
from django.shortcuts import render
from .models import Article
def article_list(request):
# 1. Retrieve the full QuerySet
qs = Article.objects.order_by('-published_at')
# 2. Create a Paginator instance
paginator = Paginator(qs, 25) # 25 items per page
# 3. Get the requested page number from query string
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
# 4. Render template
return render(request, 'articles/list.html', {"page_obj": page_obj})
To check that the SQL contains OFFSET, run the view in a local environment and enable Django’s django-debug-toolbar or inspect django.db.connection.queries after the request:
# In a Django shell
from django.test import Client
c = Client()
response = c.get('/articles/?page=10')
print(django.db.connection.queries[-1]['sql']) # Look for "OFFSET 225"
Risk: If the table grows to millions of rows, page 10 may become slow. Monitor the query time and consider switching to cursor pagination for large datasets.
2. Using DRF CursorPagination for an API Feed
Ideal for a mobile app showing a chronological feed that should remain stable even when new articles arrive.
# settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination',
'PAGE_SIZE': 20,
}
# views.py
from rest_framework import viewsets
from .serializers import ArticleSerializer
from .models import Article
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.order_by('-published_at')
serializer_class = ArticleSerializer
# CursorPagination is automatically applied via settings.
Validation steps:
- Run the API endpoint with a client (e.g., curl):
curl -s http://localhost:8000/api/articles/. - Inspect the response JSON. It should contain
nextandpreviouscursor links. - Check the database query in the Django shell:
print(Article.objects.order_by('-published_at').query). It should show aWHERE id > ?clause rather than anOFFSET. - Measure response times for page 1 vs. page 1000. CursorPagination should keep times roughly constant.
Note: CursorPagination requires the underlying QuerySet to have a unique, monotonically increasing field (commonly id or created_at). If you order by a non‑unique field, cursor pagination will raise a ImproperlyConfigured error.
Practical Checklist Before Choosing
- Are users required to jump to arbitrary pages? Yes → Prefer Paginator or PageNumberPagination.
- Is the dataset >100k rows and performance critical? Yes → Prefer CursorPagination.
- Do you need to expose a REST API with consistent ordering under concurrent writes? Yes → CursorPagination.
- Is the underlying DB a simple SQLite for prototyping? Yes → Paginator is fine; cursor performance gains are negligible.
- Do you need to support both web UI and API with the same pagination logic? Consider using DRF’s PageNumberPagination for API and Paginator for UI.
Limitations & Caveats
- Offset pagination can lead to duplicate or missing items if rows are inserted or deleted during navigation. Mitigate by locking the table (rarely recommended) or by using cursor pagination.
- Cursor pagination does not provide a way to directly retrieve page 42. If you need that, you must implement a custom cursor that encodes the offset or maintain a mapping.
- Cursor pagination requires a stable ordering field and cannot be used with
QuerySetthat change order across requests. - Large OFFSET values can cause full index scans, dramatically increasing DB load. Monitor
EXPLAINplans for heavy queries.
Conclusion
Choose Paginator (or PageNumberPagination) when users need direct page access and the dataset is moderate. Opt for CursorPagination when you serve large feeds, need constant‑time navigation, and can tolerate the lack of direct page jumps. Validate your choice by inspecting generated SQL and benchmarking response times on a representative data set.
Diagram
| Label |
|---|
| Paginator |
| PageNumberPagination |
| CursorPagination |
| Database Query |
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.