Back to Blog

Mastering Pagination in Django REST Framework: A Complete Guide

January 6, 2026
Bhavesh Rathod
6 min read
DjangoPythonREST APIBackendPagination

Mastering Pagination in Django REST Framework: A Complete Guide

When building APIs that return large datasets, pagination is essential for performance and user experience. Without it, your API could return thousands of records in a single response, causing slow load times and memory issues.

In this guide, we'll explore all pagination strategies available in Django REST Framework (DRF) and when to use each one.

Why Pagination Matters

Consider an e-commerce API returning all products:

# Without pagination - BAD class ProductListView(APIView): def get(self, request): products = Product.objects.all() # Could be 100,000+ records! serializer = ProductSerializer(products, many=True) return Response(serializer.data)

Problems with this approach:

  • Slow database queries
  • High memory consumption
  • Long response times
  • Poor user experience
  • Potential server crashes

Setting Up DRF Pagination

First, let's configure pagination globally in your Django settings:

# settings.py REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 20, }

1. PageNumber Pagination

The most common and intuitive pagination style. Users navigate using page numbers like ?page=2.

Basic Implementation

# pagination.py from rest_framework.pagination import PageNumberPagination class StandardResultsSetPagination(PageNumberPagination): page_size = 20 page_size_query_param = 'page_size' max_page_size = 100

Using in Views

# views.py from rest_framework import generics from .pagination import StandardResultsSetPagination class ProductListView(generics.ListAPIView): queryset = Product.objects.all() serializer_class = ProductSerializer pagination_class = StandardResultsSetPagination

Response Format

{ "count": 1000, "next": "http://api.example.com/products/?page=2", "previous": null, "results": [ {"id": 1, "name": "Product 1", "price": 29.99}, {"id": 2, "name": "Product 2", "price": 49.99} ] }

Custom PageNumber Pagination

# pagination.py from rest_framework.pagination import PageNumberPagination from rest_framework.response import Response class CustomPageNumberPagination(PageNumberPagination): page_size = 20 page_size_query_param = 'limit' max_page_size = 100 def get_paginated_response(self, data): return Response({ 'pagination': { 'total_items': self.page.paginator.count, 'total_pages': self.page.paginator.num_pages, 'current_page': self.page.number, 'page_size': self.get_page_size(self.request), 'has_next': self.page.has_next(), 'has_previous': self.page.has_previous(), }, 'links': { 'next': self.get_next_link(), 'previous': self.get_previous_link(), }, 'results': data })

Response with custom format:

{ "pagination": { "total_items": 1000, "total_pages": 50, "current_page": 1, "page_size": 20, "has_next": true, "has_previous": false }, "links": { "next": "http://api.example.com/products/?page=2", "previous": null }, "results": [...] }

2. LimitOffset Pagination

Similar to SQL's LIMIT and OFFSET. Users specify how many records to skip and how many to return: ?limit=20&offset=40.

Implementation

# pagination.py from rest_framework.pagination import LimitOffsetPagination class StandardLimitOffsetPagination(LimitOffsetPagination): default_limit = 20 limit_query_param = 'limit' offset_query_param = 'offset' max_limit = 100

Response Format

{ "count": 1000, "next": "http://api.example.com/products/?limit=20&offset=40", "previous": "http://api.example.com/products/?limit=20&offset=0", "results": [...] }

When to Use LimitOffset

  • When you need flexible record retrieval
  • For infinite scroll implementations
  • When integrating with systems that use offset-based queries

Drawbacks

# LimitOffset becomes slow with large offsets # This query is inefficient: Product.objects.all()[100000:100020] # Scans 100,000 rows first!

3. Cursor Pagination

The most performant option for large datasets. Uses an opaque cursor string instead of page numbers.

Why Cursor Pagination?

PageNumber/LimitOffset: O(n) - Gets slower with higher pages
Cursor Pagination: O(1) - Constant time regardless of position

Implementation

# pagination.py from rest_framework.pagination import CursorPagination class ProductCursorPagination(CursorPagination): page_size = 20 ordering = '-created_at' # Must be a unique, sequential field cursor_query_param = 'cursor'

Response Format

{ "next": "http://api.example.com/products/?cursor=cD0yMDIzLTA1LTEwKzEyJTNBMzAlM0EwMA%3D%3D", "previous": null, "results": [...] }

When to Use Cursor Pagination

  • Large datasets (millions of records)
  • Real-time feeds (social media, news)
  • When data changes frequently
  • When you need consistent performance

Important Considerations

# The ordering field must be: # 1. Unique or nearly unique (timestamps work well) # 2. Sequential (sortable) # 3. Immutable (shouldn't change after creation) class ProductCursorPagination(CursorPagination): ordering = '-created_at' # Good: timestamp is sequential # ordering = 'name' # Bad: not unique, not sequential

Comparison Chart

FeaturePageNumberLimitOffsetCursor
Performance (large data)PoorPoorExcellent
Jump to specific pageYesYesNo
Consistent with data changesNoNoYes
SEO friendlyYesPartialNo
Infinite scrollPartialGoodBest
Implementation complexityLowLowMedium

Advanced: Dynamic Pagination

Sometimes you need different pagination for different scenarios:

# pagination.py from rest_framework.pagination import PageNumberPagination, CursorPagination class DynamicPagination: """ Returns different pagination based on query params or data size """ @staticmethod def get_pagination_class(request, queryset): # Use cursor pagination for large datasets if queryset.count() > 10000: return CursorPagination # Use page number for smaller datasets return PageNumberPagination
# views.py from rest_framework import generics from .pagination import DynamicPagination class ProductListView(generics.ListAPIView): queryset = Product.objects.all() serializer_class = ProductSerializer def get_pagination_class(self): return DynamicPagination.get_pagination_class( self.request, self.get_queryset() )

Performance Optimization Tips

1. Use select_related and prefetch_related

class ProductListView(generics.ListAPIView): def get_queryset(self): return Product.objects.select_related( 'category', 'brand' ).prefetch_related( 'tags', 'images' )

2. Add Database Indexes

# models.py class Product(models.Model): name = models.CharField(max_length=255) created_at = models.DateTimeField(auto_now_add=True, db_index=True) price = models.DecimalField(max_digits=10, decimal_places=2, db_index=True) class Meta: indexes = [ models.Index(fields=['created_at', 'id']), # For cursor pagination models.Index(fields=['-price', 'name']), # For sorted queries ]

3. Use Only Required Fields

class ProductListSerializer(serializers.ModelSerializer): class Meta: model = Product fields = ['id', 'name', 'price', 'thumbnail'] # Only essential fields class ProductDetailSerializer(serializers.ModelSerializer): class Meta: model = Product fields = '__all__' # All fields for detail view

Testing Pagination

# tests.py from rest_framework.test import APITestCase from rest_framework import status class ProductPaginationTest(APITestCase): def setUp(self): # Create 50 test products for i in range(50): Product.objects.create( name=f'Product {i}', price=i * 10 ) def test_pagination_returns_correct_page_size(self): response = self.client.get('/api/products/') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['results']), 20) def test_pagination_returns_total_count(self): response = self.client.get('/api/products/') self.assertEqual(response.data['count'], 50) def test_custom_page_size(self): response = self.client.get('/api/products/?page_size=10') self.assertEqual(len(response.data['results']), 10) def test_page_navigation(self): response = self.client.get('/api/products/?page=2') self.assertIsNotNone(response.data['previous']) self.assertIsNotNone(response.data['next'])

Conclusion

Choosing the right pagination strategy depends on your use case:

  • PageNumber: Best for traditional web apps with page navigation
  • LimitOffset: Good for flexible queries and infinite scroll with smaller datasets
  • Cursor: Essential for large datasets and real-time feeds

Remember to:

  1. Always paginate list endpoints
  2. Add appropriate database indexes
  3. Use select_related and prefetch_related
  4. Test pagination with realistic data volumes

Implementing proper pagination will significantly improve your API's performance and user experience.

Further Reading