Skip to content

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.

python
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.

python
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 = ArticleLimitOffsetPagination

UnorderedObjectListWarning

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:

json
{
    "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_classThe Django Paginator class to use for pagination.django.core.paginator.Paginator
page_sizeA numeric value indicating the page size.PAGE_SIZE setting
page_query_paramA string value indicating the name of the query parameter to use for the pagination control.'page'
page_size_query_paramA 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_sizeIf 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_stringsA list or tuple of string values indicating values that may be used with the page_query_param to request the final page.('last',)
templateThe 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:

json
{
    "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_limitA numeric value indicating the limit to use if one is not provided by the client in a query parameter.PAGE_SIZE setting
limit_query_paramA string value indicating the name of the "limit" query parameter.'limit'
offset_query_paramA string value indicating the name of the "offset" query parameter.'offset'
max_limitIf set, this is a numeric value indicating the maximum allowable limit that may be requested by the client. (Highly recommended).None
templateThe 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:

json
{
    "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 ordering field 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 count of 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_sizeA numeric value indicating the page size.PAGE_SIZE setting
cursor_query_paramA string value indicating the name of the "cursor" query parameter.'cursor'
orderingThis 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_paramA 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_sizeIf set, this is a numeric value indicating the maximum allowable requested page size. (Highly recommended if page_size_query_param is set).None
offset_cutoffA numeric value indicating the maximum allowable offset. Guard against malicious users attempting to cause expensive database queries by jumping too far ahead.1000
templateThe 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.

python
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 = StandardResultsSetPagination

Custom 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 a Response instance.

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.

python
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.