DRF Pagination

Choose the right pagination style — PageNumber, LimitOffset or Cursor — and know why Cursor wins at scale.

An endpoint that returns every row works fine with 50 records and falls over at 5 million. Pagination is not optional on a real API. DRF ships three styles, and picking the right one is a scaling decision, not a cosmetic one.

StyleBest forWeakness
PageNumberPaginationSmall/medium sets, page-number UIsDeep pages are slow (large OFFSET)
LimitOffsetPaginationFlexible windows, ‘load 20 from N’Same OFFSET cost; items shift if data changes
CursorPaginationLarge, frequently-changing feedsNo jump-to-page; needs a stable ordering

Set a global default, override per view

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

Why cursor pagination matters: offset-based paging does OFFSET 100000 LIMIT 20, which forces the database to walk and discard 100,000 rows — slower the deeper you go. Cursor pagination instead says WHERE created < last_seen ORDER BY created LIMIT 20, which uses an index and stays fast at any depth. It also won't skip or duplicate rows when new items are inserted mid-scroll — exactly what you want for an infinite feed.

Example

Example · python
from rest_framework.pagination import CursorPagination, PageNumberPagination
from rest_framework import generics


class StandardResultsPagination(PageNumberPagination):
    page_size = 25
    page_size_query_param = 'page_size'   # let clients ask for more...
    max_page_size = 100                    # ...but cap it to protect the DB


class ActivityFeedPagination(CursorPagination):
    page_size = 30
    ordering = '-created'      # MUST be an indexed, stable ordering
    cursor_query_param = 'cursor'


class ActivityFeedView(generics.ListAPIView):
    serializer_class = ActivitySerializer
    pagination_class = ActivityFeedPagination   # per-view override

    def get_queryset(self):
        return (
            Activity.objects
            .filter(user=self.request.user)
            .select_related('actor', 'target')
            .order_by('-created', '-id')   # tiebreaker keeps the cursor stable
        )

# A cursor response is opaque and cheap to page forever:
# {
#   "next": "https://api/feed/?cursor=cD0yMDI2LTA3",
#   "previous": null,
#   "results": [ ... 30 items ... ]
# }
# No 'count' is issued — cursor pagination skips the expensive COUNT(*) too.

When to use it

  • A developer configures PageNumberPagination globally so every DRF list endpoint returns 20 items per page.
  • A team uses CursorPagination for a real-time feed so clients can scroll forward without page-number drift.
  • A developer writes a custom LimitOffsetPagination subclass to cap the maximum page size at 100 records.

More examples

PageNumberPagination globally

Applies page-number pagination with 20 items per page to every DRF list endpoint by default.

Example · python
# settings.py
REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 20,
}

Custom pagination class

Defines a reusable pagination class with a configurable page_size query parameter capped at 50.

Example · python
from rest_framework.pagination import PageNumberPagination

class SmallResultsSetPagination(PageNumberPagination):
    page_size = 5
    page_size_query_param = "page_size"
    max_page_size = 50

class PostListView(ListAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    pagination_class = SmallResultsSetPagination

CursorPagination for feeds

Uses cursor-based pagination for a chronological feed to avoid inconsistencies from new insertions.

Example · python
from rest_framework.pagination import CursorPagination

class FeedCursorPagination(CursorPagination):
    page_size = 10
    ordering = "-published_at"

class FeedView(ListAPIView):
    queryset = Post.objects.filter(draft=False)
    serializer_class = PostSerializer
    pagination_class = FeedCursorPagination

Discussion

  • Be the first to comment on this lesson.