Choosing Django REST Framework Pagination for List Endpoints
Compare PageNumberPagination, LimitOffsetPagination, and CursorPagination to pick the right DRF pagination style for your list endpoints.
11 Jul 2025, 12:26 UTC

Decision: match pagination to your data shape, not to a default
Django REST Framework (DRF) paginates list endpoints by default, but the default class is not always the right choice. The wrong choice can make responses slow as tables grow, break client interfaces that expect total counts, or force users to click through pages on an append-only feed. This guide helps you choose among DRF's three built-in pagination styles and verify the one you select.
Useful takeaway: use PageNumberPagination for small-to-medium tables with page links, LimitOffsetPagination only for modest infinite-scroll lists, and CursorPagination for large append-only feeds where stable forward ordering matters more than jumping to page 42.
Constraints to check first
- Dataset size and expected growth
- Whether clients need a total record count
- Required ordering stability, especially for cursor pagination
- Client UI pattern: numbered pages, infinite scroll, or continuous feed
- Database indexes on ordering fields
Supported options compared
| Class | Query parameters | Best for | Key limitation |
|---|---|---|---|
PageNumberPagination | ?page=2 | Small-to-medium datasets; direct page linking | Performance declines on very large tables; count query on every response |
LimitOffsetPagination | ?limit=10&offset=20 | Infinite-scroll UIs over moderate datasets | High offsets force the database to scan and discard skipped rows |
CursorPagination | ?cursor=... (opaque) | Large append-only feeds with fixed ordering | Forward-only paging; no arbitrary page jumps or total count |
Trade-offs
PageNumberPagination
This is DRF's default. It returns count, next, previous, and results. It is intuitive and easy to bookmark, but the count query can become expensive on large tables, and deep pages are slower than early ones.
LimitOffsetPagination
Clients control page size with limit and starting point with offset. It fits infinite scroll, but as offset grows the database must read and discard more rows before returning the slice. Use it only when offsets stay relatively small or when the ordering column is well indexed.
CursorPagination
Returns an opaque cursor token instead of a page number. It is efficient for large append-only data because it seeks from the last seen position rather than skipping rows. It requires a deterministic, unique ordering, for example -created plus a tie-breaker such as id. It does not support total counts or jumping to an arbitrary page.
Implementation
Set project-wide defaults
Edit your project settings file, typically settings.py, inside the REST_FRAMEWORK dictionary:
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 25,
"MAX_PAGE_SIZE": 100,
}
Make this edit in your application directory with normal file-edit permissions. Changing these defaults alters every list view that does not override pagination_class, so review affected endpoints after deploying.
Override per view
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.viewsets import ModelViewSet
class ItemViewSet(ModelViewSet):
queryset = Item.objects.all()
serializer_class = ItemSerializer
pagination_class = LimitOffsetPagination
Place this in your views module. No database migration is required, but changing a view's pagination class changes its response shape.
Cursor pagination example
from rest_framework.pagination import CursorPagination
from rest_framework.viewsets import ModelViewSet
class FeedCursorPagination(CursorPagination):
page_size = 20
ordering = "-created, id"
cursor_query_param = "cursor"
class FeedViewSet(ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
pagination_class = FeedCursorPagination
The ordering attribute must produce a stable, unique sequence. If two rows share the same created timestamp, the id tie-breaker keeps the cursor deterministic.
Validation
Check active defaults
Inspect settings without starting the server:
$ python -c "import django, os; os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings'); django.setup(); from django.conf import settings; print(settings.REST_FRAMEWORK.get('DEFAULT_PAGINATION_CLASS'))"
Replace project.settings with your actual settings module. Expected output is the dotted path to the active pagination class.
Test a list endpoint
Run the development server locally, then call a list view:
$ curl -i "http://localhost:8000/api/items/?page=2"
No special permissions are required beyond local project access. Confirm the JSON body includes count, next, previous, and results. Do not assume these keys exist; verify them for each pagination class.
Test cursor pagination
$ curl -i "http://localhost:8000/api/feed/?cursor=..."
Replace ... with the cursor value returned in the previous response's next field. Confirm the response includes next and previous cursors and does not include count.
Limitations
CursorPaginationcannot return total counts and does not support arbitrary page navigation.LimitOffsetPaginationslows down as offset increases on large tables.- Default behavior depends on your project's
REST_FRAMEWORKsettings and DRF version; verifyPAGE_SIZEandMAX_PAGE_SIZErather than assuming defaults.
Practical result check
After changing pagination, hit a list endpoint and inspect the response keys. For page-number pagination, confirm count is present. For cursor pagination, confirm count is absent and that following next returns new rows without repetition. If behavior does not match, revert the REST_FRAMEWORK or view-level pagination_class change and redeploy.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.