Skip to content

Pagination in REST APIs — Complete Guide

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Pagination in REST APIs. We cover key concepts, practical examples, and best practices to help you master this topic.

Pagination in REST APIs splits large result sets into manageable pages, using offset-based pagination with page numbers or cursor-based pagination with opaque tokens for consistent iteration.

flowchart TD
  A[Pagination Types] --> B[Offset-Based]
  A --> C[Cursor-Based]
  A --> D[Keyset Pagination]
  B --> B1[?page=2&per_page=20]
  B --> B2[Simple but inconsistent]
  C --> C1[?cursor=abc123]
  C --> C2[Consistent across changes]
  style B fill:#fff9c4
  style C fill:#c8e6c9

Offset-based pagination uses ?page=2&per_page=20. The server returns items 20-39. It is simple but has a problem: if items are inserted or deleted between requests, the same item may appear on two pages or be skipped. Cursor-based pagination uses an opaque token pointing to a specific item: ?cursor=abc123&limit=20. It returns items after the cursor, which is stable even when data changes.

Think of offset pagination like reading a book where page numbers can shift if pages are added or removed. Cursor pagination is like using a bookmark. You always continue from where you left off, regardless of what changes around you.

Example: Offset-Based Pagination

import requests

def get_users_page(page, per_page=10):
    response = requests.get(
        "https://api.example.com/users",
        params={"page": page, "per_page": per_page}
    )
    data = response.json()
    total = response.headers.get("X-Total-Count")
    print(f"Page {page}: {len(data)} users (total: {total})")
    return data

# Fetch first 3 pages
for p in range(1, 4):
    get_users_page(p)

Expected output:

Page 1: 10 users (total: 150)
Page 2: 10 users (total: 150)
Page 3: 10 users (total: 150)

Example: Cursor-Based Pagination

import requests

def get_users_cursor(cursor=None, limit=10):
    params = {"limit": limit}
    if cursor:
        params["cursor"] = cursor
    response = requests.get(
        "https://api.example.com/users",
        params=params
    )
    data = response.json()
    next_cursor = data.get("next_cursor")
    print(f"Got {len(data['items'])} users, next: {next_cursor}")
    return next_cursor

# Fetch all pages using cursor
cursor = None
page_num = 0
while cursor is not None or page_num == 0:
    cursor = get_users_cursor(cursor)
    page_num += 1
    if page_num >= 5 or not cursor:
        break

Expected output:

Got 10 users, next: abc123
Got 10 users, next: def456
Got 10 users, next: ghi789
Got 10 users, next: None

Example: Pagination with Link Header

import requests

response = requests.get(
    "https://api.example.com/users",
    params={"page": 2, "per_page": 10}
)
link_header = response.headers.get("Link")
print(f"Link header: {link_header}")

# Parse Link header
links = {}
for part in link_header.split(", "):
    section = part.split("; ")
    url = section[0].strip("<>")
    rel = section[1].split("=")[1].strip("\"")
    links[rel] = url
print(f"First: {links.get('first')}")
print(f"Prev: {links.get('prev')}")
print(f"Next: {links.get('next')}")
print(f"Last: {links.get('last')}")

Expected output:

Link header: <https://api.example.com/users?page=1&per_page=10>; rel="first", <https://api.example.com/users?page=1&per_page=10>; rel="prev", <https://api.example.com/users?page=3&per_page=10>; rel="next", <https://api.example.com/users?page=15&per_page=10>; rel="last"
First: https://api.example.com/users?page=1&per_page=10
Prev: https://api.example.com/users?page=1&per_page=10
Next: https://api.example.com/users?page=3&per_page=10
Last: https://api.example.com/users?page=15&per_page=10

Common Mistakes

  1. Not providing pagination at all — Returning all results without pagination causes memory issues on both server and client for large datasets.
  2. Using offset pagination for real-time data — Offset pagination skips or duplicates items when data changes between requests. Use cursor-based pagination for dynamic datasets.
  3. Not setting a maximum per_page — Letting clients request 10000 items per page defeats the purpose of pagination and can crash the server.
  4. Forgetting pagination metadata — Include total count, page number, and next/prev links so clients can navigate without guessing.
  5. Inconsistent pagination parameter names — Using page/per_page in some endpoints and offset/limit in others forces clients to handle both patterns.

Practice Questions

  1. What is the main advantage of cursor pagination over offset pagination?
  2. What headers are used for pagination links in REST APIs?
  3. Why should you limit the maximum items per page?
  4. When would you choose offset pagination over cursor pagination?
  5. Challenge: Implement a cursor-based pagination system in Python that generates opaque cursors encoding the last seen ID and a timestamp. Decode cursors to resume pagination.

FAQ

What is the difference between offset and page-based pagination?

Offset skips N records: ?offset=20&limit=10. Page calculates offset: ?page=3&per_page=10 (offset=20). Page-based is more user-friendly.

How do I implement cursor pagination?

Encode the last item's identifier (like ID or timestamp) into an opaque string. The server decrypts it and returns items after that identifier.

Should I use X-Total-Count header or include count in the body?

Both work. The Link header with pagination URLs and X-Total-Count is a common pattern. Including metadata in the response body is also fine.

How do I handle pagination for real-time data?

Use cursor-based pagination with timestamps. This ensures consistent results even when new items are being added.

What is keyset pagination?

Keyset pagination uses a WHERE clause on a unique column: WHERE id > 100 LIMIT 10. It is efficient but requires knowing the last seen value.

Mini Project

Build a Python pagination utility class that supports both offset-based and cursor-based pagination. It should generate pagination URLs, calculate offsets, encode/decode cursors, and include metadata in the response envelope.

What's Next

Now learn about HATEOAS in REST API Design.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro