Custom PageNumberPagination in Django REST Framework: Configure, Test, and Avoid Common Pitfalls
Create a custom PageNumberPagination subclass in Django REST Framework to control page size, query parameters, and response format. Learn how to configure, test, and avoid common pitfalls with concrete code examples and verification steps.
03 Aug 2026, 08:12 UTC

Why Create a Custom PageNumberPagination?
When building an API with Django REST Framework (DRF), the default PageNumberPagination is often enough, but real‑world clients need more control:
- Limit the maximum number of items a client can request.
- Expose a query parameter that lets the consumer choose page size.
- Rename the
pageparameter for clarity. - Shape the response to match a front‑end framework’s expectations.
Defining a custom subclass lets you bundle these tweaks into a single, reusable component.
Concrete Example: A Subclassed PageNumberPagination
# pagination.py
from rest_framework.pagination import PageNumberPagination
class CustomPageNumberPagination(PageNumberPagination):
# Default size for every request
page_size = 20
# Query parameter that lets clients override the size
page_size_query_param = "size"
# Prevent clients from requesting more than 100 items
max_page_size = 100
# Optional: change the name of the page query param
page_query_param = "p"
Hook it into a viewset:
# views.py
from rest_framework import viewsets
from .models import YourModel
from .serializers import YourModelSerializer
from .pagination import CustomPageNumberPagination
class YourModelViewSet(viewsets.ModelViewSet):
queryset = YourModel.objects.all()
serializer_class = YourModelSerializer
pagination_class = CustomPageNumberPagination
With this in place, a client can request ?p=2&size=50 and receive up to 50 items per page, but no more than 100.
How DRF Handles the Request
When a GET request hits the viewset, DRF:
- Instantiates the
CustomPageNumberPaginationclass. - Reads
request.query_params.get("size", None)to determine the requested page size. - Clamps the value between
page_sizeandmax_page_size. - Calculates
offset = (page_number - 1) * page_sizeand slices the queryset. - Serializes the slice and wraps it in a JSON structure that includes
nextandpreviouslinks.
Resulting JSON Structure
{
"count": 1250,
"next": "http://api.example.com/items/?p=3&size=50",
"previous": "http://api.example.com/items/?p=1&size=50",
"results": [
{"id": 51, "name": "Item 51"},
...
]
}
Limitations to Keep in Mind
- Memory Footprint:
PageNumberPaginationloads an entire page into memory. Withpage_size=1000, this can consume significant RAM on a busy instance. - Data Mutability: In highly concurrent environments, page boundaries can shift as new rows are inserted or deleted, leading to duplicates or gaps between pages.
- Query Complexity: If the queryset includes complex
select_relatedorprefetch_relatedcalls, the cost of fetching each page grows linearly with page size.
Common Mistakes and How to Avoid Them
- Forgetting
max_page_size: Without a cap, a malicious client could request thousands of items, causing a denial‑of‑service. Always setmax_page_sizeto a reasonable value. - Hard‑coding
page_sizebut not exposing a query param: Clients cannot adjust the load, which reduces flexibility and can lead to large responses if the default is too high. - Using
PageNumberPaginationon endpoints that return millions of records: the server may run out of memory or take a long time to compute offsets.
Practical Verification Steps
- Spin up a test project:
# In your Django project directory python manage.py startapp api # Add 'rest_framework' and 'api' to INSTALLED_APPS - Create a simple model, serializer, and viewset (see example above).
- Apply migrations and populate data:
python manage.py makemigrations python manage.py migrate # Create 200 demo items via Django shell or fixture - Run the dev server:
python manage.py runserver - Test the endpoint using curl or a browser:
curl "http://127.0.0.1:8000/api/items/?p=2&size=50"Check that the JSON contains 50 items, and that
nextandpreviousURLs are present. - Verify max size enforcement:
curl "http://127.0.0.1:8000/api/items/?p=1&size=1000"The response should contain 100 items (the
max_page_sizecap). - Monitor memory during a large request (e.g.,
size=1000) usingtoporhtopon the server. If memory spikes, reducepage_sizeor switch toCursorPagination.
When to Prefer CursorPagination
If your data set is frequently updated or extremely large, consider CursorPagination. It uses an opaque cursor that encodes the position in the result set, guaranteeing stable ordering without loading an entire page. However, it requires a stable ordering field (e.g., id or a timestamp) and changes the URL format (e.g., ?cursor=abc123).
Takeaway
Subclassing PageNumberPagination gives you fine‑grained control over page size, query parameters, and response structure while keeping your view logic unchanged. Be mindful of memory usage, enforce a max_page_size, and test thoroughly in a realistic environment. For mutable or massive datasets, evaluate CursorPagination as a more robust alternative.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.