← All writing

Give cursor pagination a deterministic ordering

Laravel 13 / PHP 8.3+Sources checked 2026-09-14

A growing activity feed becomes slow with deep offset pages. Cursor pagination can help, but it needs an ordering that identifies where the next page begins. A nonunique timestamp alone leaves ties ambiguous.

Add a unique tie-breaker

$events = Event::query()
    ->where('tenant_id', $tenant->id)
    ->orderBy('created_at')
    ->orderBy('id')
    ->cursorPaginate(30);

This example assumes non-null ordering fields on the queried table and an appropriate index for the tenant and order. Review Laravel's cursor limitations for expressions and nullable ordered values before adapting the query.

Cursors change the navigation contract

A cursor is a position, not an arbitrary page number. That fits an activity feed with next/previous navigation, but may not fit a report where users expect to jump directly to page fifty. Choose the interaction deliberately.

Concurrent inserts or edits can still affect what a user sees across requests. If you need a fixed export snapshot, a pagination method alone does not provide one. Establish an explicit cutoff or snapshot strategy.

Test several rows with identical timestamps and traverse across a page boundary. Verify no tie is lost or repeated under the expected stable dataset. Then test the intended behavior when new rows arrive. Keep tenant filtering on every cursor request; an encoded cursor is not an authorization token and must not decide which account's feed the caller can read.

Reference

Official documentation.