Pagination
When an API endpoint returns a large list of records, returning all of them in a single response can lead to slow response times and high memory usage on both the server and the client. To solve this, DRF provides customizable pagination styles, allowing you to split large result sets into individual pages of data.
Default Behavior
By default, DRF's DEFAULT_PAGINATION_CLASS and PAGE_SIZE settings are set to None. This means that pagination is disabled by default and a list endpoint will return the entire queryset.
ViewSets & Generic Views
Pagination is only performed automatically if you are using GenericAPIView subclasses (like ListAPIView) or GenericViewSet descendants (like ModelViewSet or ReadOnlyModelViewSet). Be aware that a plain APIView or ViewSet does not include automatic pagination and you must handle it manually.
Setting the Pagination Style
The pagination style can be configured globally in your settings.py. This keeps your code DRY by applying the same pagination logic across all generic views and viewsets, rather than defining it manually everywhere.
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 100
}You can also override the global pagination class on an individual view by setting the pagination_class attribute. If you want to disable pagination for a specific view when a global class is set, you can set pagination_class = None.
from rest_framework import generics
from rest_framework.pagination import LimitOffsetPagination
from .models import Article
from .serializers import ArticleSerializer
class ArticleLimitOffsetPagination(LimitOffsetPagination):
default_limit = 50
class ArticleListView(generics.ListAPIView):
queryset = Article.objects.order_by('-created_at')
serializer_class = ArticleSerializer
pagination_class = ArticleLimitOffsetPaginationUnorderedObjectListWarning
Pagination requires a consistently ordered queryset. If you paginate an unordered queryset, you will see an UnorderedObjectListWarning and your pages may return inconsistent results across requests. Always ensure your views or models define an explicit ordering (e.g., queryset = Article.objects.order_by('-created_at')).
Built-in Pagination Styles
DRF provides several built-in pagination classes, each suited for different use cases.
PageNumberPagination
The PageNumberPagination class accepts a single page number in the request query parameters. This is the most common form of pagination and is familiar to most users.
Request: GET https://api.example.org/accounts/?page=4
Response:
{
"count": 1023,
"next": "https://api.example.org/accounts/?page=5",
"previous": "https://api.example.org/accounts/?page=3",
"results": [
// ...
]
}Configuration:
You can customize the behavior of PageNumberPagination by setting any of the following attributes:
| Attribute | Description | Default |
|---|---|---|
django_paginator_class | The Django Paginator class to use for pagination. | django.core.paginator.Paginator |
page_size | A numeric value indicating the page size. | PAGE_SIZE setting |
page_query_param | A string value indicating the name of the query parameter to use for the pagination control. | 'page' |
page_size_query_param | A string value indicating the name of the query parameter that allows the client to set the page size on a per-request basis. | None |
max_page_size | If set, this is a numeric value indicating the maximum allowable requested page size. (Highly recommended if page_size_query_param is set). | None |
last_page_strings | A list or tuple of string values indicating values that may be used with the page_query_param to request the final page. | ('last',) |
template | The name of a template to use when rendering pagination controls in the browsable API. | 'rest_framework/pagination/numbers.html' |
LimitOffsetPagination
The LimitOffsetPagination class accepts limit and offset parameters, mimicking SQL LIMIT and OFFSET syntax. This is particularly useful when clients need to fetch a specific chunk of data.
If the client omits the limit parameter, it defaults to the default_limit attribute. If the offset parameter is omitted, it defaults to 0. Note that if default_limit is not set and the global PAGE_SIZE setting is None, pagination will be disabled and the endpoint will return an unpaginated list.
Request: GET https://api.example.org/accounts/?limit=100&offset=400
Response:
{
"count": 1023,
"next": "https://api.example.org/accounts/?limit=100&offset=500",
"previous": "https://api.example.org/accounts/?limit=100&offset=300",
"results": [
// ...
]
}Configuration:
You can customize the behavior of LimitOffsetPagination by setting any of the following attributes:
| Attribute | Description | Default |
|---|---|---|
default_limit | A numeric value indicating the limit to use if one is not provided by the client in a query parameter. | PAGE_SIZE setting |
limit_query_param | A string value indicating the name of the "limit" query parameter. | 'limit' |
offset_query_param | A string value indicating the name of the "offset" query parameter. | 'offset' |
max_limit | If set, this is a numeric value indicating the maximum allowable limit that may be requested by the client. (Highly recommended). | None |
template | The name of a template to use when rendering pagination controls in the browsable API. | 'rest_framework/pagination/numbers.html' |
CursorPagination
The CursorPagination class presents an opaque "cursor" indicator that the client may use to page through the result set.
Request: GET https://api.example.org/accounts/?cursor=cD0xNSZvPWFjY291bnRfaWQ%3D
Response:
{
"next": "https://api.example.org/accounts/?cursor=cD0xNSZvPWFjY291bnRfaWQ%3D",
"previous": null,
"results": [
// ...
]
}Why use Cursor Pagination?
Cursor pagination is highly efficient for extremely large datasets compared to offset-based pagination. Offset-based pagination (like LimitOffsetPagination and PageNumberPagination) becomes slower as the table grows because the database must scan and skip the offset rows. Cursor pagination avoids this: the cursor encodes a position value and the query becomes a WHERE ordering_field > value filter instead of an OFFSET n row skip. However, this performance win requires that your database has an index on the ordering field.
Constraints:
- Cursor pagination requires an
orderingfield that is immutable, non-null and effectively unique (like creation timestamps or monotonic IDs). - It does not allow navigating to arbitrary pages (e.g., jumping straight to page 10).
- It cannot report the total
countof results in the response, which is often a showstopper for certain UI requirements.
Configuration:
You can customize the behavior of CursorPagination by setting any of the following attributes:
| Attribute | Description | Default |
|---|---|---|
page_size | A numeric value indicating the page size. | PAGE_SIZE setting |
cursor_query_param | A string value indicating the name of the "cursor" query parameter. | 'cursor' |
ordering | This should be a string, or list of strings, indicating the field against which the cursor based pagination will be applied. For example: ordering = ('-created_at', '-pk'). | '-created' |
page_size_query_param | A string value indicating the name of the query parameter that allows the client to set the page size on a per-request basis. | None |
max_page_size | If set, this is a numeric value indicating the maximum allowable requested page size. (Highly recommended if page_size_query_param is set). | None |
offset_cutoff | A numeric value indicating the maximum allowable offset. Guard against malicious users attempting to cause expensive database queries by jumping too far ahead. | 1000 |
template | The name of a template to use when rendering pagination controls in the browsable API. | 'rest_framework/pagination/previous_and_next.html' |
Modifying the Pagination Style
To alter the default configuration of any of the built-in styles, you can create a custom subclass of the desired pagination class. You can then assign your custom class to DEFAULT_PAGINATION_CLASS globally or pagination_class on a specific view.
The same subclassing pattern applies across all built-in styles, though the available attributes differ per class.
from rest_framework.pagination import PageNumberPagination
class StandardResultsSetPagination(PageNumberPagination):
page_size = 100
page_size_query_param = 'page_size'
max_page_size = 1000
# Applying it to a view
class ArticleListView(generics.ListAPIView):
queryset = Article.objects.order_by('-created_at')
serializer_class = ArticleSerializer
pagination_class = StandardResultsSetPaginationCustom Pagination
Creating a New Pagination Style
To create a completely custom pagination style from scratch, you can subclass BasePagination and override:
.paginate_queryset(self, queryset, request, view=None): This method is passed the initial queryset and should return an iterable object. That object contains only the data in the requested page..get_paginated_response(self, data): This method is passed the serialized page data and should return aResponseinstance.
Modifying the Response Format
Instead of building a paginator from scratch, a more common use case is changing the paginated response structure of an existing style. You can do this by subclassing one of the built-in classes (like PageNumberPagination) and overriding just the .get_paginated_response() method.
OpenAPI Schemas
If you override .get_paginated_response(), your OpenAPI schema will become stale. To fix this, you must also override the .get_paginated_response_schema() method to describe your new response structure.
The following example replaces the default count, next and previous keys with a single links object.
from rest_framework import pagination
from rest_framework.response import Response
class CustomResponsePagination(pagination.PageNumberPagination):
def get_paginated_response(self, data):
return Response({
'links': {
'next': self.get_next_link(),
'previous': self.get_previous_link()
},
'count': self.page.paginator.count,
'results': data
})
def get_paginated_response_schema(self, schema):
return {
'type': 'object',
'properties': {
'links': {
'type': 'object',
'properties': {
'next': {
'type': 'string',
'nullable': True,
'format': 'uri',
},
'previous': {
'type': 'string',
'nullable': True,
'format': 'uri',
}
},
},
'count': {
'type': 'integer',
'example': 123,
},
'results': schema,
},
}You can then apply this custom pagination class globally or to specific views.
OpenAPI 3.1 Compatibility
The 'nullable': True syntax used above matches DRF's built-in schema generation for OpenAPI 3.0. If you are generating an OpenAPI 3.1 schema (e.g., via modern versions of drf-spectacular), this syntax is deprecated. Instead, you should use an array of types: 'type': ['string', 'null'].
TIP
You can find third-party packages for more advanced pagination styles in the official DRF documentation.
