Pagination is a technique for loading data in pages, used in mobile applications and web services to handle large sets of records. According to Android Developers Documentation (2025), proper pagination implementation reduces API load, saves traffic, and improves user experience. Page-by-page loading allows an application to display content gradually without waiting for all data to load at once.
Key Takeaways
Pagination (from Latin pagination — page division) is a technique of splitting a large dataset into sequential portions (pages). In mobile applications, pagination is used when loading message lists, news feeds, product catalogs, order history, and any other collections with a potentially unlimited number of records.
Without pagination, an application has to load all data at once, leading to long wait times, high traffic consumption, and unstable performance on low-end devices. An API request with pagination returns only one portion of data along with meta-information for loading the next one — thus, the application controls the amount of information it receives.
The main pagination metrics are: page size — the number of records per page (usually 10–50), and page number or cursor — a pointer to the current position in the dataset. The choice of page size depends on the type of data: for compact elements (names) 20–30 is enough, for cards with images — 10–15.
Mobile devices have limited resources: RAM, CPU speed, and traffic limits. Pagination solves three key tasks: reduced memory consumption (only visible elements are stored in memory), faster first render (the first portion loads faster than the entire dataset), and traffic savings (data is loaded only when the user scrolls the list).
There are four main types of pagination, each solving specific tasks. The choice of method depends on data consistency requirements, API architecture, storage type, and acceptable implementation complexity on the client and server sides.
| Type | How It Works | Stability | Speed on Large Volumes |
|---|---|---|---|
| Offset | LIMIT + OFFSET in SQL | low | degrades with growing OFFSET |
| Cursor | WHERE id > last_id | high | stable (O(log n)) |
| Keyset | WHERE key > last_key | high | stable (O(log n)) |
| Time-based | WHERE created_at < last_time | medium | stable with an index |
Offset pagination is suitable for static or rarely updated datasets where simple implementation matters. Cursor and Keyset are for dynamic data with frequent insertions. Time-based is for chronological feeds where records are ordered by creation time. The GraphQL standard Relay uses cursor pagination as the only recommended method.
Offset pagination is the simplest type of page-based loading. The client passes page and limit (or offset and limit) parameters, and the server applies SQL OFFSET and LIMIT. For example, page=2, limit=20 returns records 21 through 40. This method is intuitive and easy to implement on any stack.
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items")
async def get_items(
page: int = Query(default=1, ge=1),
limit: int = Query(default=20, le=100)
):
offset = (page - 1) * limit
items = await fetch_items(offset, limit)
total = await count_items()
return {
"items": items,
"total": total,
"page": page,
"pages": (total + limit - 1) // limit
}
The main drawback of Offset pagination is the problem of missing and duplicate records. If new records are added to the table between two requests, the OFFSET shifts: the user may see the same record twice or miss a new one. This is critical for news feeds and chats where consistency matters.
Another problem is performance degradation on large OFFSET values. The database has to scan and skip the first offset records before returning the result. At offset=100000 even with LIMIT 20, the server will spend noticeable time scanning. PostgreSQL and MySQL show a linear drop in speed as OFFSET grows.
Offset pagination remains the best choice for: admin panels (data rarely changes, page navigation is needed), reports and historical logs (fixed data snapshot), filtered catalogs (you can jump to any page). Offset is also the simplest to implement on the client — RecyclerView with Paging 3 supports it out of the box.
Keyset pagination uses a unique key (usually the primary key) to filter records. Instead of OFFSET, the query uses WHERE id > last_seen_id. This ensures stable performance regardless of the number of records and no duplicates on insertions, since new records always have a larger id.
-- Offset pagination (problematic)
SELECT * FROM posts
ORDER BY id
LIMIT 20 OFFSET 100;
-- Keyset pagination (stable)
SELECT * FROM posts
WHERE id > 100
ORDER BY id
LIMIT 20;
Time-based pagination (or cursor by time) uses the created_at timestamp for navigation. The client passes the timestamp of the last loaded record, and the server returns records created before or after that timestamp. This method is popular in social networks and news feeds where the order of records is determined by publication time.
A feature of Time-based pagination is possible duplicates if two records are created in the same millisecond. To eliminate this issue, combine a time-based key with a unique id: WHERE (created_at, id) < (last_time, last_id). Such a composite cursor guarantees uniqueness of each record and precise ordering.
Keyset pagination requires a column with a unique and monotonically increasing value (auto-increment id, UUID v7). Time-based is suitable for any table with created_at but requires additional duplicate handling. The main difference: Keyset works stably under any insert operations, while Time-based is sensitive to identical timestamps.
The choice of pagination type depends on the nature of the data and user experience requirements. Below are recommendations for typical scenarios in mobile development. There is no universal solution — each method has a domain where it is optimal.
The Android Paging 3 library supports all pagination types through PagingSource. For Offset — PagingSource with Int key (page), for Cursor — with String or Long key (cursor). PagingSource automatically manages loading, caching, and retry on errors.
class PostPagingSource(
private val api: PostApi
) : PagingSource<Long, Post>() {
override suspend fun load(
params: LoadParams<Long>
): LoadResult<Long, Post> {
val cursor = params.key ?: Long.MAX_VALUE
return try {
val response = api.getPosts(cursor, params.loadSize)
LoadResult.Page(
data = response.items,
prevKey = null,
nextKey = response.items.lastOrNull()?.id
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Long, Post>): Long? {
return state.anchorPosition?.let {
state.closestItemToPosition(it)?.id
}
}
}
Page size affects loading speed and perceived performance. For mobile applications, the optimal range is 10–25 items per page. Fewer than 10 results in too many API requests and janky scrolling. More than 25 results in slow initial load on slow networks.
For images and videos, reduce page size to 5–10, since each element takes additional time to load media. For text-based lists (comments, logs), the size can be increased to 30–50 records. It is recommended to make page size configurable via the API so the client can adapt to different network conditions.
Frequently Asked Questions
Pagination is loading data in portions rather than all at once. Like a book: you read one page, then turn to the next. In an app, this means that when you scroll through a list, the next batch of data loads, not the entire list at once, saving traffic and memory.
Offset counts records: “skip 20, return the next 10.” If a new record is added between loads, the numbering breaks. Cursor uses the unique identifier of the last record: “return 10 records after ID = 100.” New records do not affect the position.
For mobile applications, 10–25 items is optimal. For image-heavy lists — 5–10, for text feeds — 20–30. The size depends on the average element size: the heavier the element, the smaller the page should be for fast display.
Use the Paging 3 library from Android Jetpack. It provides PagingSource for loading, PagingData for reactive streams, and PagingDataAdapter for automatic loading on scroll. The library supports Offset, Cursor, and Keyset pagination through a custom PagingSource.
Infinite scroll is a UI pattern where a new batch of data loads automatically as the user approaches the end of the list. Pagination is the mechanism of loading data in batches, while infinite scroll is one way to display it. An alternative is the “Load more” button.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also