DRF Pagination: PageNumber, LimitOffset, and Cursor Returning a flat JSON array of 500,000 database rows in a single HTTP response crashes client browsers, exhausts server memory, and degrades network latency. Pagination segments large QuerySets into manageable chunks. Django REST Framework provides three distinct pagination schemes: PageNumberPagination , LimitOffsetPagination , and CursorPagination .
Pagination Style URL Signature Database Performance Stable against Insertions? Client Experience PageNumberPagination?page=3&page_size=20Moderate (OFFSET) No (duplicate/skipped items)Classic page numbers (1, 2, 3) LimitOffsetPagination?limit=25&offset=50Moderate (OFFSET) No (page drift on insert)Flexible custom slice loading CursorPagination?cursor=cD0yMDI2LTA...Optimal (O(1) B-tree) Yes (100% resilient) Infinite scroll, live feeds
1 # api/pagination.py
2 from rest_framework . pagination import PageNumberPagination
3 from rest_framework . response import Response
4
5 class StandardResultsSetPagination ( PageNumberPagination ) :
6 page_size = 20
7 page_size_query_param = "page_size" # Allows client to override page size
8 max_page_size = 100 # Protects against DoS via ?page_size=100000
9 page_query_param = "page"
Response Envelope:
1 {
2 "count" : 1420 ,
3 "next" : "https://api.enterprise.com/v1/articles/?page=3" ,
4 "previous" : "https://api.enterprise.com/v1/articles/?page=1" ,
5 "results" : [
6 { "id" : 41 , "title" : "Understanding WebSockets" }
7 ]
8 }
Maps directly to SQL LIMIT and OFFSET:
1 from rest_framework . pagination import LimitOffsetPagination
2
3 class CustomLimitOffsetPagination ( LimitOffsetPagination ) :
4 default_limit = 25
5 limit_query_param = "limit"
6 offset_query_param = "offset"
7 max_limit = 100
Client Query:
1 GET /api/v1/articles/?limit=10&offset=40 # Fetches items 41 through 50
Both PageNumber and LimitOffset rely on SQL OFFSET. As OFFSET increases into millions (OFFSET 1000000), the database must scan and discard 1,000,000 rows, crippling performance. Furthermore, if a new item is inserted while a user browses page 1, items shift, causing duplicate entries on page 2.
CursorPagination uses an opaque encoded pointer referencing a specific row (e.g. timestamp or ID):
Uses SQL WHERE created_at < cursor LIMIT 20 (Instant B-Tree lookup!). 100% immune to missing or duplicate items during real-time writes. 1 from rest_framework . pagination import CursorPagination
2
3 class FeedCursorPagination ( CursorPagination ) :
4 page_size = 25
5 cursor_query_param = "cursor"
6 ordering = "-created_at" # Ordering MUST be deterministic!
Response:
1 {
2 "next" : "https://api.enterprise.com/v1/feed/?cursor=cD0yMDI2LTA5LTE1VDA0..." ,
3 "previous" : null ,
4 "results" : [ ... ]
5 }
Notice: count is omitted because calculating SELECT COUNT(*) on massive tables is expensive!
1 class ArticleViewSet ( ModelViewSet ) :
2 queryset = Article . objects . all ( )
3 serializer_class = ArticleSerializer
4 pagination_class = StandardResultsSetPagination
Interactive Quiz • Knowledge Check
Practice Quiz Why does SQL OFFSET become slow on deep pagination (e.g. ?page=10000)?
A Browsers refuse to open high page numbers
B The database engine must scan and discard the first 200,000 rows before returning the requested 20 rows, consuming heavy CPU and I/O
C Page numbers cannot exceed 100
D Python runs out of RAM
What is the primary advantage of CursorPagination over PageNumberPagination?
A CursorPagination displays page numbers 1, 2, 3
B It uses index-based lookups (WHERE created_at < x) rather than offsets, ensuring O(1) query speeds and preventing duplicate/skipped items when new rows are inserted in real time
C It converts JSON to CSV
D It only works with SQLite
Why is max_page_size critical on custom PageNumberPagination classes?
A To prevent clients from requesting ?page_size=500000 and triggering a Denial of Service (DoS) by forcing the database and server memory to serialize massive payloads
B It is required by HTML5
C To delete excess database records
D To enable caching
Why does CursorPagination omit the total count property from its JSON response?
A Counting records is forbidden in REST
B Executing SELECT COUNT(*) across millions of rows requires a full table or index scan, defeating the performance optimization of cursor pagination
C It is a bug in DRF
D JavaScript cannot parse counts
How can a client disable pagination for a specific request if allowed by the view?
A By passing ?pagination=off (if page_size_query_param is configured and None/null is sent)
B By deleting the URL
C By using HTTP PUT
D Pagination can never be disabled