
August 14, 2026
9 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Choosing between cursor and offset pagination is one of the most consequential architectural decisions you make when designing a REST API. This API Pagination Cursor vs Offset Deep Dive breaks down the mechanical differences, performance characteristics, and real-world trade-offs based on shipping production APIs since 2010. If you are building a data-heavy application or integrating with modern frontend frameworks, understanding these distinctions prevents costly refactors later; for broader API design context, see my guide on Laravel API best practices.
How does offset pagination work and when does it fail?
Offset pagination is the traditional approach most developers learn first. It uses two parameters: limit (page size) and offset (records to skip). The database executes LIMIT 20 OFFSET 1000, meaning it must scan and discard 1,000 rows before returning the next 20. For small tables or administrative interfaces where users rarely navigate past page five, this works perfectly well. The SQL is simple, human-readable, and supported by every ORM without special configuration.
The problems emerge at scale. On a table with millions of rows, requesting page 50,000 forces MySQL or PostgreSQL to process 999,980 rows just to return 20 results. Query time degrades linearly with the offset value. I have seen this cause timeout errors on legal-tech portals where case archives grew beyond initial projections. Additionally, offset pagination suffers from data drift: if a new record is inserted while a user is browsing, items shift between pages, causing duplicates or skipped records. For stable, high-performance feeds, this model fundamentally breaks down.
How does cursor pagination solve performance and consistency issues?
Cursor pagination replaces the numeric offset with an opaque pointer to the last seen record. Instead of "skip 1,000 rows," the query asks for "20 rows where id > 58392." The database uses an index seek directly to that position, making query time constant regardless of how deep into the dataset you are. This is why platforms like Twitter, Facebook, and GitHub use cursor-based approaches for their primary feeds.
Beyond raw speed, cursors provide stability. Because the query anchors to a specific record rather than a positional offset, inserts and deletes between requests do not cause items to shift or duplicate. The user sees a consistent stream. The trade-off is that you lose random access: there is no "page 47" concept. Navigation becomes strictly sequential (next/previous), which aligns perfectly with infinite scroll and mobile-first interfaces but frustrates users who need to jump to arbitrary positions.
Implementing cursor pagination in Laravel 12
Laravel 12 provides a built-in CursorPaginator that handles encoding, decoding, and query construction automatically. Here is a production-ready controller method:
<?php
namespace App\Http\Controllers\Api;
use App\Models\CaseFile;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class CaseFileController extends Controller
{
public function index(Request $request): JsonResponse
{
$paginator = CaseFile::query()
->where('firm_id', $request->user()->firm_id)
->orderByDesc('file_date')
->orderByDesc('id') // Tie-breaker for deterministic ordering
->cursorPaginate(20);
return response()->json([
'data' => $paginator->items(),
'meta' => [
'next_cursor' => $paginator->nextCursor()?->encode(),
'prev_cursor' => $paginator->previousCursor()?->encode(),
'per_page' => $paginator->perPage(),
],
]);
}
} The critical detail is the secondary sort column. When sorting by a non-unique column like file_date, multiple records share the same value. Without a unique tie-breaker (id), the cursor cannot reliably determine position, and records will be skipped or duplicated. Always include a unique column as the final sort criterion.
What are the practical trade-offs between cursor and offset pagination?
No pagination strategy is universally superior. The right choice depends on your product's navigation patterns, dataset size, and user expectations. Below is a comparison based on real implementation experience across eCommerce catalogs, legal document archives, and booking systems.
| Criterion | Offset Pagination | Cursor Pagination |
|---|---|---|
| Random page access | ✅ Supported natively | ❌ Not possible without hacks |
| Performance at depth | ❌ Degrades linearly | ✅ Constant time regardless of position |
| Data consistency | ❌ Prone to drift/duplicates | ✅ Stable across concurrent writes |
| Total count availability | ✅ Cheap COUNT(*) query | ❌ Expensive or estimated only |
| Implementation complexity | ✅ Trivial in any framework | ⚠️ Requires sortable unique key + encoding |
| SEO / crawlability | ✅ Clean ?page=N URLs | ⚠️ Opaque cursors less friendly |
| Best fit | Admin panels, search results, small datasets | Feeds, timelines, infinite scroll, large datasets |
In practice, many applications use both. An admin dashboard for managing court marriage applications benefits from offset pagination because staff need to jump to specific pages and total counts matter for reporting. The same system's public-facing case search feed should use cursors because users scroll continuously and performance must remain predictable as the archive grows. When architecting a new system, consider whether different endpoints warrant different strategies rather than enforcing one globally. For teams evaluating full-stack approaches, the full-stack developer landscape in Nepal increasingly demands fluency in both patterns.
How do you handle edge cases and common pitfalls in cursor pagination?
Cursor pagination introduces subtleties that catch developers off guard during production deployments. Addressing these proactively prevents support tickets and data inconsistencies.
Handling multi-column sorts and composite cursors
When your API supports sorting by multiple columns (e.g., status then created_at), the cursor must encode all sort columns plus the unique tie-breaker. Laravel's cursorPaginate handles this automatically when you chain orderBy calls correctly, but custom implementations must construct compound WHERE clauses:
-- Composite cursor for status + created_at + id
SELECT * FROM cases
WHERE (status, created_at, id) > ('active', '2026-08-10 14:30:00', 58392)
ORDER BY status ASC, created_at DESC, id DESC
LIMIT 20; MySQL 8.0+ and PostgreSQL support row-value comparisons natively. Older MySQL versions require expanded OR conditions, which are harder to optimize. Verify your database version supports this syntax before committing to composite cursors in production.
Managing cursor opacity and security
Cursors should be opaque to clients. Never expose raw IDs or timestamps in the cursor string; encode them using base64 or signed tokens. Laravel's default encoder uses base64, which is sufficient for preventing accidental tampering but not cryptographically secure. If cursor manipulation could leak unauthorized records (e.g., cross-tenant data access in a SaaS platform), implement signed cursors or validate decoded values against authorization policies server-side. I have audited legal-tech portals where unsigned cursors allowed users to enumerate case files belonging to other firms by incrementing encoded IDs.
Dealing with deleted anchor records
If the record referenced by a cursor is deleted before the next request, the query returns zero results even though more data exists. Handle this gracefully: detect empty result sets with valid cursors and either return a descriptive error prompting the client to refresh, or implement fallback logic that seeks to the nearest valid position. For most applications, returning an empty set with a cursor_expired flag in the meta block gives frontends enough information to recover without confusing users.
How do you benchmark and validate pagination performance in production?
Theoretical complexity analysis matters, but real-world performance depends on your specific schema, indexes, and data distribution. Before committing to a pagination strategy for a high-traffic endpoint, run targeted benchmarks.
- Generate realistic test data. Use Laravel factories or seeders to populate tables with production-scale volumes (1M+ rows for cursor candidates). Uniform distributions mask problems that appear with skewed real-world data.
- Profile queries at multiple depths. Test offset pagination at page 1, 100, 1,000, and 10,000. Test cursor pagination at equivalent positions. Record p95 latency, not averages.
- Measure under concurrent load. Use tools like k6 or wrk to simulate realistic traffic. Offset pagination often shows acceptable single-query latency but collapses under concurrency due to repeated full scans competing for I/O.
- Validate index coverage. Run
EXPLAIN ANALYZE(PostgreSQL) orEXPLAIN FORMAT=JSON(MySQL) to confirm cursor queries use index seeks, not scans. Missing composite indexes silently degrade cursor performance to offset-like behavior. - Monitor in production. Add query tags or APM instrumentation distinguishing paginated queries. Set alerts for p95 latency exceeding thresholds. I track this on legal-tech portals where case file listings must respond within 200ms at any depth.
For teams building APIs in Nepal's growing tech sector, investing in proper benchmarking infrastructure pays dividends. The cost difference between a well-indexed cursor query and a deep offset scan can be the difference between Rs 500/month (~USD 4) and Rs 5,000/month (~USD 37) in database hosting for high-traffic endpoints. Understanding these trade-offs is part of being a competent Laravel developer in Nepal working on real production systems.
Making the Right Choice for Your API Pagination Strategy
This API Pagination Cursor vs Offset Deep Dive has covered the mechanics, trade-offs, implementation patterns, and validation approaches that separate theoretical knowledge from production-ready decisions. Neither strategy is inherently superior; each serves distinct product requirements. Offset pagination remains the right choice for administrative interfaces, search results with faceted navigation, and any context where users expect traditional page numbers. Cursor pagination earns its place in feeds, timelines, mobile-first infinite scroll experiences, and any endpoint where dataset growth would otherwise cause unacceptable latency degradation.
The most common mistake I see is not choosing at all—defaulting to offset everywhere because it is familiar, then discovering performance problems after launch when refactoring is expensive. Make the decision deliberately during API design, document the rationale in your codebase, and benchmark before shipping. If you are building a Laravel API and need guidance on pagination strategy, database optimization, or production deployment architecture, reach out to discuss your project.

