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.