Last modified: Dec 02, 2025 By Alexander Williams

FastAPI REST API Pagination Guide

Pagination is key for modern APIs. It splits large datasets into pages.

This improves performance and user experience. FastAPI makes it simple.

This guide covers two main methods. You will learn offset and cursor pagination.

Why Pagination Matters

Returning all data in one response is bad. It strains servers and clients.

Network transfer times increase. Mobile users face high data costs.

Pagination solves these issues. It delivers data in manageable chunks.

This is crucial for scalable applications. It is a standard REST API feature.

Project Setup

First, create a virtual environment. Then install FastAPI and an ASGI server.


pip install fastapi uvicorn sqlalchemy databases

We will use a simple database model. It represents a list of products.

For complex async database setups, see our guide on FastAPI Async Database with asyncpg SQLAlchemy.

Offset-Based Pagination

This is the classic "page" and "size" approach. It uses skip and limit.

Clients request a specific page number. The server calculates which items to skip.

It is simple and intuitive. But it has drawbacks with very large datasets.

Implementing Offset Pagination

Define query parameters for page and size. Use them in your database query.


from fastapi import FastAPI, Query
from typing import Optional, List

app = FastAPI()

# Simulated database
fake_items_db = [{"id": i, "name": f"Item {i}"} for i in range(1, 101)]

@app.get("/items/")
async def read_items(
    page: int = Query(1, ge=1, description="Page number"),
    size: int = Query(10, ge=1, le=100, description="Items per page")
):
    """Endpoint with offset pagination."""
    # Calculate skip and limit
    skip = (page - 1) * size
    limit = size

    # Get paginated slice
    items = fake_items_db[skip : skip + limit]

    # Calculate total pages
    total_items = len(fake_items_db)
    total_pages = (total_items + size - 1) // size  # Ceiling division

    return {
        "items": items,
        "page": page,
        "size": size,
        "total_items": total_items,
        "total_pages": total_pages,
        "has_next": page < total_pages,
        "has_prev": page > 1
    }

The skip formula finds the start index. The limit sets the page size.

The response includes helpful metadata. Clients know if more pages exist.

Example Request and Output

Call the endpoint with page=2 and size=5.


curl "http://localhost:8000/items/?page=2&size=5"

{
  "items": [
    {"id": 6, "name": "Item 6"},
    {"id": 7, "name": "Item 7"},
    {"id": 8, "name": "Item 8"},
    {"id": 9, "name": "Item 9"},
    {"id": 10, "name": "Item 10"}
  ],
  "page": 2,
  "size": 5,
  "total_items": 100,
  "total_pages": 20,
  "has_next": true,
  "has_prev": true
}

The output is clear and structured. It shows items 6 to 10.

Cursor-Based Pagination

This method uses a pointer to the last item. It is better for real-time data.

It avoids the skip problem in offset pagination. Performance is more consistent.

The client uses a cursor from the previous response. It fetches the next set of records.

Implementing Cursor Pagination

Use a unique, sequential field like id or created_at. The cursor marks where to start.


from fastapi import FastAPI, Query
from typing import Optional

app = FastAPI()

@app.get("/items-cursor/")
async def read_items_cursor(
    cursor: Optional[int] = Query(None, description="Last item ID from previous page"),
    limit: int = Query(10, ge=1, le=100, description="Items per page")
):
    """Endpoint with cursor pagination."""
    # In a real app, query: WHERE id > cursor
    if cursor is None:
        cursor = 0

    # Filter items where id > cursor
    filtered_items = [item for item in fake_items_db if item["id"] > cursor]

    # Apply limit
    items = filtered_items[:limit]

    # Determine next cursor
    next_cursor = items[-1]["id"] if items else None

    return {
        "items": items,
        "next_cursor": next_cursor,
        "limit": limit,
        "has_more": len(items) == limit and next_cursor is not None
    }

The cursor parameter is the last seen ID. The query fetches items after that ID.

This method is efficient for large tables. Database indexes work well with WHERE id > cursor.

Example Request and Output

Start without a cursor to get the first page.


curl "http://localhost:8000/items-cursor/?limit=3"

{
  "items": [
    {"id": 1, "name": "Item 1"},
    {"id": 2, "name": "Item 2"},
    {"id": 3, "name": "Item 3"}
  ],
  "next_cursor": 3,
  "limit": 3,
  "has_more": true
}

Use the next_cursor to get the next page.


curl "http://localhost:8000/items-cursor/?cursor=3&limit=3"

{
  "items": [
    {"id": 4, "name": "Item 4"},
    {"id": 5, "name": "Item 5"},
    {"id": 6, "name": "Item 6"}
  ],
  "next_cursor": 6,
  "limit": 3,
  "has_more": true
}

Choosing the Right Method

Offset Pagination is simple. It works for static datasets.

Users can jump to any page. It is good for admin panels.

But it gets slow with large offsets. Database must count through skipped rows.

Cursor Pagination is more efficient. It is perfect for infinite scroll.

It handles real-time data inserts well. Use it for feeds and timelines.

The downside is no random page access. You must move sequentially.

Best Practices and Tips

Always set a maximum page size. This prevents server overload.

Use sensible defaults. Page 1 and size 20 are common.

Include metadata in responses. Total counts and links help clients.

For streaming very large datasets, consider our guide on Stream Large Responses with Python FastAPI.

Optimize database queries with indexes. This is critical for performance.

Learn more in our FastAPI Performance Optimization Guide.

Common Pitfalls

Avoid offset with large skip values. Database performance will drop.

Never expose raw database keys as cursors. Use encoded or opaque cursors.

Validate all input parameters. Ensure page and size are positive numbers.

Handle edge cases. Empty results or a cursor pointing to a deleted item.

Conclusion

Pagination is essential for good API design. FastAPI makes implementation straightforward.

Offset pagination is easy for basic use cases. Cursor pagination scales better.

Choose based on your data and user needs. Always include helpful response metadata.

This improves both performance and developer experience. Your API will be robust and scalable.