aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Web Development

Laravel Pagination: Offset vs Cursor Paging Guide

Rendering thousands of records on a single list screen chokes both the browser and the database. That is exactly where Laravel pagination comes in: it splits query results into pages and fetches only the slice you need on each request. Laravel does this with three different methods, and the one you pick directly affects both performance and user experience. In this article I explain the difference between offset-based and cursor-based pagination, when to use each, and how to implement them with real, working code.

Three pagination methods: paginate, simplePaginate, cursorPaginate

Eloquent and the query builder ship with three ready-made methods. The difference is how much information they show the user and how much load they put on the database:

  • paginate() — Classic offset pagination. It also counts the total number of records, so you can render numbered page links like "1 2 3 … 50" and show the total page count.
  • simplePaginate() — Still offset based, but it does not count the total. It only produces "Previous / Next" buttons; without the count query it is faster.
  • cursorPaginate() — Cursor-based pagination. Instead of an offset it references the column values of the last row; on very large tables this is the fastest and most consistent method.

How offset pagination works

The offset approach uses SQL LIMIT and OFFSET. With pages of 20, when you request page 5 Laravel produces LIMIT 20 OFFSET 80. It is the most common and easiest method:

// Controller
public function index()
{
    $posts = \App\Models\Post::query()
        ->where('published', true)
        ->orderByDesc('created_at')
        ->paginate(20);

    return view('posts.index', compact('posts'));
}

On the Blade side you render the links in one line. You can customise the Tailwind or Bootstrap view with php artisan vendor:publish --tag=laravel-pagination:

<div class="posts">
    @foreach ($posts as $post)
        <article>{{ $post->title }}</article>
    @endforeach
</div>

{{ $posts->links() }}

The upside: users can jump directly to any page and see how many pages there are in total. The downside shows up as you scale.

The two big problems with offset

As the database grows, offset causes two headaches:

  • Slowing queries: When you say OFFSET 100000, the database still has to scan the 100,000 rows it is about to skip. Deep pages slow down noticeably.
  • Shifting records: If a new record is inserted while you are on page 2, every row shifts down by one. On the next page you either see a record you already saw, or skip one entirely.

Because lists are usually sorted "newest to oldest" and new content is added constantly, this shifting issue happens more often than you might think.

Cursor pagination: the right choice for large tables

Cursor pagination uses a reference point instead of an offset. It says "give me the rows after this one" and continues with a WHERE clause, so there is no scanning of skipped rows. Laravel encrypts the cursor value and puts it in the URL:

$posts = \App\Models\Post::query()
    ->where('published', true)
    ->orderByDesc('id')
    ->cursorPaginate(20);

The query generated behind the scenes looks roughly like this; there is no offset, just an indexed comparison:

select * from posts
where published = 1 and id < 480
order by id desc
limit 21;

Important rules:

  • Sorting is required and must be stable: the orderBy field must be unique (for example id). If you sort by a field with duplicate values, add a tie-breaking second column: orderBy('created_at')->orderBy('id').
  • No jumping to a random page: a cursor only moves forward/backward; you cannot offer a "page 8" link. It is ideal for infinite scroll and "Load more" buttons.
  • No total count: like simplePaginate, it does not count, which is why it is fast.

When to use which

The decision is actually simple and clears up with two questions: does the user need to jump to numbered pages, and how big is the table?

  • Admin panels, search results: page numbers and totals are expected → paginate().
  • Mobile streams, social feeds, "Load more": forward infinite scroll → cursorPaginate().
  • Simple lists, very large table but no numbers needed: simplePaginate() or cursor.

Rule of thumb: on tables reaching hundreds of thousands of rows, switch to cursor where possible; but if numbered navigation is genuinely required by the product, stay on offset and back your queries with indexes.

API and performance tips

If you are writing a JSON API, returning the paginator directly is enough; Laravel generates the data, links and meta fields automatically. It also works with API Resources:

return \App\Http\Resources\PostResource::collection(
    Post::where('published', true)->cursorPaginate(20)
);

A few practical recommendations:

  • Add a database index on the columns you sort and filter by; the speed of cursor depends on it.
  • Select only the columns you need: select('id', 'title', 'created_at').
  • Eager-load related data with with() so you do not blow up into N+1 queries per page.
  • If the page size comes from user input, set an upper bound (e.g. max 100).

Frequently Asked Questions

Can I show the total page count with cursor pagination?

No. Because cursor (and simplePaginate) does not run a count query, it does not know the total record or page count. If you absolutely need the total, use offset-based paginate(), or run the count as a separate, cached query.

Can I turn cursorPaginate output into numbered page links?

No. By nature a cursor can only move to adjacent pages (previous/next); it cannot jump to a random page. If numbered navigation is mandatory, you have to stay on offset pagination.

Why does offset slow down on very large tables?

The OFFSET N command tells the database to find the first N rows and throw them away. As N grows, the number of rows to skip grows too, so the workload increases. Cursor, by contrast, starts directly at the right point with an indexed WHERE clause, so it stays at a constant speed.

Getting your pagination architecture right is the foundation of an application that scales. If you want to talk about migrating from offset to cursor in your project, or optimising slow list queries, get in touch with me.

Bu kategorideki tüm yazılar →

Devamı için