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
wherecondition on an ordered column instead ofoffset. 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?
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?
<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():
$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?
$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.
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:
$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_atalone can skip rows with the same timestamp. - No ordering. Pagination without
orderBycan 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?
- Open
/postsand confirm ten rows and a Next link appear. - Click Next and confirm the URL has
?page=2(or acursorvalue). - Add
DB::listen()or use Telescope to confirm thatsimplePaginateruns nocount(*)query. - 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.



