Requesting a list of 10,000 products in one call is a classic mistake our engineers see in every third project. The API returns 40 MB of JSON, the app freezes while parsing, and the user sees a white screen for 8 seconds—then leaves. We implement pagination at the contract level between client and server: this reduces load by 3–5 times and provides smooth navigation without delays. Evaluate your API's pagination—contact us for an audit.
Pagination Implementation: Offset or Cursor?
Most developers reach for offset pagination by default: ?page=2&limit=20. It works while the data is static. But add a live feed where records are inserted at the top, and the user on page 3 misses entries or sees duplicates: an INSERT at the start shifts all offsets. On tables with 500,000+ rows, offset scans up to 100,000 rows to shift the cursor—queries take 200–400 ms.
Cursor-based pagination solves this: the server returns next_cursor, the client passes it in the next request. The cursor is an opaque token (usually base64 of id + timestamp) that fixes the position in the dataset. PostgreSQL backends implement it with WHERE id < :cursor ORDER BY id DESC LIMIT 20. No duplicates, no gaps. Query speeds remain stable—50–100 ms regardless of table size.
| Criterion | Offset Pagination | Cursor Pagination |
|---|---|---|
| Implementation simplicity | High | Medium (requires cursor on backend) |
| Duplicates/missing on insert | Yes | No |
| Performance on large tables | Degrades (O(N)) | Stable (O(1)) |
| Support for jumping to arbitrary page | Yes (page=3) | Difficult (only sequential scroll) |
| Caching | Easy (pages by offset) | Requires storing cursors |
Why Cursor Pagination Is More Efficient Than Offset?
Cursor pagination is 3 times faster than offset on tables from 100,000 rows—benchmarks show response time dropping from 300 ms to 90 ms. It also guarantees data consistency under concurrent inserts. For news feeds, chat messages, and frequently updated catalogs, it's the mandatory choice.
How to Set Up Caching with RemoteMediator?
On Android with Paging 3, cursor pagination is implemented via RemoteMediator + PagingSource:
class ItemPagingSource( private val api: ItemsApi, private val query: String ) : PagingSource<String, Item>() { override suspend fun load(params: LoadParams<String>): LoadResult<String, Item> { return try { val response = api.getItems( cursor = params.key, limit = params.loadSize, query = query ) LoadResult.Page( data = response.items, prevKey = null, nextKey = response.nextCursor ) } catch (e: HttpException) { LoadResult.Error(e) } } override fun getRefreshKey(state: PagingState<String, Item>): String? { return state.anchorPosition?.let { anchor -> state.closestPageToPosition(anchor)?.nextKey } } } LazyColumn in Compose connects via collectAsLazyPagingItems()—a ready-made binding that handles Loading, Error, NotLoading states without extra code.
On iOS, the equivalent is a compositional layout with UICollectionViewDiffableDataSource and NSDiffableDataSourceSnapshot. Prefetch is implemented via UICollectionViewDataSourcePrefetching: the method prefetchItemsAt is called N cells before the edge, firing a network request ahead of time. Without prefetch, at scroll speeds above 500 px/s, empty cells appear—the user waits 1–2 seconds for loading.
How to Implement Cursor Pagination on Android: Step-by-Step
- Define the API contract: the server returns
next_cursorin the JSON response. - Create
PagingSource<String, Item>that passes the cursor inloadParams.key. - Connect
RemoteMediatorfor offline cache:RemoteMediatorfetches data from the network and saves it to Room. - In
PagingSource, implementgetRefreshKeyto restore position after a refresh. - Set up
LazyColumnwithcollectAsLazyPagingItems()and an explicitLoadStateFooterwith a retry button.
Caching Strategy Comparison: RemoteMediator vs. Simple Cache
| Parameter | RemoteMediator + Room | Simple Cache (LruCache) |
|---|---|---|
| Offline access | Yes | No |
| Consistency on updates | ETag/Last-Modified | No mechanism |
| Implementation complexity | Medium | Low |
| Traffic savings | 60–80% | 0% |
| Recommendation | High-load projects | Prototypes, static data |
Infinite Scroll and Pull-to-Refresh
Infinite scroll without an error state is a typical problem. The network drops on page 5, request hangs, user scrolls down and nothing happens. An explicit LoadStateFooter with a retry button is needed.
In Paging 3:
adapter.addLoadStateListener { loadState -> binding.retryButton.isVisible = loadState.source.append is LoadState.Error binding.progressBar.isVisible = loadState.source.append is LoadState.Loading } Pull-to-refresh resets pagination to the first page via adapter.refresh()—Paging 3 invalidates PagingSource and starts fresh. On SwiftUI, it's the refreshable modifier that calls invalidateQueries() in TCA or updates the @StateObject view model.
Cache and Offline
Paging 3 + Room is the standard offline-first combo. RemoteMediator writes data to the local DB, PagingSource reads from Room rather than the network. The user sees data even without internet; fresh data loads in the background.
Key point: cache invalidation strategy. If the server updates a record, the local copy becomes stale. Solution: ETag or Last-Modified in response headers—the client sends If-None-Match, server returns 304 with no body if unchanged. Room updates only changed records via @Insert(onConflict = OnConflictStrategy.REPLACE). This reduces downloaded data by 60–80%.
What's Included in the Work
- Audit of the current API and pagination contract specification.
- Implementation of cursor/offset pagination on the client (Android/iOS/cross-platform).
- Integration of RemoteMediator and Room for offline mode.
- Configuration of prefetch and retry logic.
- Testing on real devices with poor network simulation.
- Code documentation and backend improvement recommendations.
- Post-implementation support: 2 weeks of consultation.
Timelines
Simple offset pagination without cache—1–2 days. Cursor pagination with RemoteMediator, offline cache, and retry logic—3–5 days. The cost is calculated individually after analyzing requirements and the existing API. We have 5+ years of experience in mobile development and over 30 projects with pagination—trust our specialists. Order a pagination audit of your app—we'll find the optimal solution.
Typical Pagination Implementation Mistakes
- Missing prefetch on iOS—empty cells on fast scroll.
- Using offset in feeds with frequent inserts—duplicates and gaps.
- Ignoring loading and error states: user sees no indicator.
- No caching—every scroll triggers a network request.
- Wrong page size: 5 items causes frequent loads, 100 causes long waits.







