Skip to content

Cursor Pagination

Cursor pagination (keyset pagination) provides better performance and consistency for large datasets compared to offset-based pagination.

FeatureOffset-basedCursor-based
Performance on large datasetsDegrades with higher pagesConsistent
Concurrent modificationsMay skip/duplicate rowsConsistent results
Random page accessSupportedNot supported
Implementation complexitySimpleModerate
use paginator_rs::{Paginator, CursorValue};
// First page (no cursor needed)
let params = Paginator::new()
.per_page(20)
.sort().asc("id")
.build();
// Next page using cursor
let params = Paginator::new()
.per_page(20)
.cursor()
.after("id", CursorValue::Int(42))
.apply()
.build();
// Previous page
let params = Paginator::new()
.per_page(20)
.cursor()
.before("id", CursorValue::Int(42))
.apply()
.build();
use paginator_rs::{PaginatorBuilder, CursorValue};
// Next page
let params = PaginatorBuilder::new()
.per_page(20)
.sort_by("id")
.cursor_after("id", CursorValue::Int(42))
.build();
// Previous page
let params = PaginatorBuilder::new()
.per_page(20)
.sort_by("id")
.cursor_before("id", CursorValue::Int(42))
.build();

The CursorValue enum supports multiple types:

use paginator_rs::CursorValue;
CursorValue::String("2024-01-01T00:00:00Z".to_string())
CursorValue::Int(42)
CursorValue::Float(3.14)
CursorValue::Uuid("550e8400-e29b-41d4-a716-446655440000".to_string())

Cursors returned in API responses are Base64-encoded JSON. You can decode them:

// From API response meta.next_cursor
let params = PaginatorBuilder::new()
.per_page(20)
.cursor_from_encoded("eyJmaWVsZCI6ImlkIiwidmFsdWUiOjQyLCJkaXJlY3Rpb24iOiJhZnRlciJ9")
.unwrap()
.build();
// Or with fluent builder
let params = Paginator::new()
.per_page(20)
.cursor()
.from_encoded("eyJmaWVsZCI6ImlkIiwidmFsdWUiOjQyLCJkaXJlY3Rpb24iOiJhZnRlciJ9")
.unwrap()
.apply()
.build();

For optimal performance with cursor pagination, skip the expensive COUNT(*) query:

let params = Paginator::new()
.per_page(20)
.sort().asc("id")
.disable_total_count()
.build();

When total count is disabled, meta.total and meta.total_pages will be None, but meta.has_next and meta.has_prev will still work correctly.

{
"data": [...],
"meta": {
"page": 1,
"per_page": 20,
"has_next": true,
"has_prev": false,
"next_cursor": "eyJmaWVsZCI6ImlkIiwidmFsdWUiOjQ0LCJkaXJlY3Rpb24iOiJhZnRlciJ9",
"prev_cursor": "eyJmaWVsZCI6ImlkIiwidmFsdWUiOjQzLCJkaXJlY3Rpb24iOiJiZWZvcmUifQ=="
}
}