Muhammad Yasir Amin

Loading websiteโ€ฆ

More from Muhammad Yasir Amin

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Subscribe on YouTube

Laravel 13 Simple Pagination Tutorial: simplePaginate vs paginate vs cursorPaginate

๐Ÿ› ๏ธ Laravel Tutorials4 min read

Laravel 13 Simple Pagination Tutorial: simplePaginate vs paginate vs cursorPaginate

Muhammad Yasir Amin

Muhammad Yasir Amin

Senior Full Stack Engineerโ€ขNov 10, 2026
Laravel 13 Simple Pagination Tutorial: simplePaginate vs paginate vs cursorPaginate

In Laravel 13, use simplePaginate() when you only need Previous and Next links. It skips the extra COUNT(*) query that paginate() runs, so it is faster on large tables. For very large or constantly changing data, cursorPaginate() is faster still because it avoids large offsets.

This tutorial targets Laravel 13 (PHP 8.3 to 8.5). The code was written for this article and not executed in a live project. Behaviour described here follows the pagination chapter of the Laravel docs.

What is the difference between paginate, simplePaginate and cursorPaginate?

  • paginate() runs a count query so it knows the total pages. You get page numbers and a "showing X of Y" total.
  • simplePaginate() fetches one extra row to know whether a next page exists. No total, no page numbers, only Previous and Next.
  • cursorPaginate() uses a where condition on an ordered column instead of offset. It needs a stable, unique order and has no page numbers.

Offset-based methods get slower the deeper the page, because the database still reads and discards the skipped rows. Cursor pagination does not have that cost.

Prerequisites

  • A Laravel 13 app with a database.
  • A model with enough rows to test, for example Post. Seed fake rows with a factory: Post::factory()->count(100)->create().

Step 1: How do you use simplePaginate in a controller?

JavaScript
namespace App\Http\Controllers;

use App\Models\Post;

class PostController extends Controller
{
    public function index()
    {
        $posts = Post::query()
            ->latest()
            ->simplePaginate(10);

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

Register the route with Route::get('/posts', [PostController::class, 'index']);.

Step 2: How do you show the links in Blade?

PHP
<ul>
    @foreach ($posts as $post)
        <li>{{ $post->title }}</li>
    @endforeach
</ul>

{{ $posts->links() }}

The paginator renders Previous and Next controls using the default Tailwind views. Switch to Bootstrap markup by calling Paginator::useBootstrapFive() in AppServiceProvider::boot().

How do you keep filters in the pagination links?

Add the current query string to each link with withQueryString():

JavaScript
$posts = Post::query()
    ->when(request('search'), fn ($q, $s) => $q->where('title', 'like', "%{$s}%"))
    ->latest()
    ->simplePaginate(10)
    ->withQueryString();

Without it, clicking Next drops the search parameter and the user sees unfiltered results.

How do you use cursorPaginate?

PHP
$posts = Post::query()
    ->orderBy('id')
    ->cursorPaginate(10);

The URL carries an encoded cursor parameter instead of page. The ordering columns must be unique together, or rows can repeat or vanish between pages, so add the primary key as a tie-breaker. Use it for infinite scroll, activity feeds and API endpoints with big tables. Do not use it if users must jump to page 37.

How do you return paginated JSON from an API?

Return the paginator directly. Laravel converts it to JSON with data, next_page_url, prev_page_url and per_page keys.

CSS
Route::get('/api/posts', function () {
    return App\Models\Post::latest()->simplePaginate(15);
});

Wrap results in an API resource with PostResource::collection($posts) when you want to control the fields.

How do you change the page size?

Let the client request a size, but cap it to protect the database:

PHP
$perPage = min((int) request('per_page', 15), 100);

$posts = Post::latest()->simplePaginate($perPage);

Common mistakes

  • Calling $posts->total() on a simple paginator. It does not know the total, so that method is not available.
  • Losing query parameters. Forget withQueryString() and filters reset on every click.
  • Cursor pagination on non-unique columns. Ordering by created_at alone can skip rows with the same timestamp.
  • No ordering. Pagination without orderBy can return rows in a different order between requests.
  • N+1 queries in the list. Eager load relationships with with() before paginating.

How do you verify it works?

  1. Open /posts and confirm ten rows and a Next link appear.
  2. Click Next and confirm the URL has ?page=2 (or a cursor value).
  3. Add DB::listen() or use Telescope to confirm that simplePaginate runs no count(*) query.
  4. Test the last page: the Next link must be disabled.

FAQ

When should I use simplePaginate instead of paginate?

When page numbers and totals are not needed, especially on big tables where the count query is slow.

Can I show page numbers with cursor pagination?

No. A cursor has no concept of page numbers.

How do I change the page query parameter name?

Pass a third argument to simplePaginate($perPage, ['*'], 'p') to use ?p=2.

References

Article Index & Topic Tags
PerformanceLaravelEloquentPagination

YouTube Channel

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Muhammad Yasir Amin
Available for Contracts
Author & Solutions Architect

Senior Full Stack Engineer

Independent Senior Full Stack Engineer working on Laravel, React and Next.js applications, APIs and delivery workflows.

Laravel & PHP ApplicationsReact & Next.js InterfacesREST APIs & IntegrationsDatabase Queries & DebuggingDocker Development WorkflowsClear Scope & Handover
Discuss Your Next Step

What Does Your Application Need Next?

Share your current system, the problem you want to solve and what a useful result looks like. We can identify the right starting point and agree on a practical scope.

Technical Audit

Find the Next Useful Fix

Application Development

Scoped, Reviewable Milestones

Ongoing Support

Agreed Capacity and Priorities

Share your goals, current stack and requirements before the call.