Laravel Cursor Pagination &amp; Lazy Collections at Scale | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    Cursor Pagination, Lazy Collections, and Chunked Iteration at Scale in Laravel        On this page       1. [  The Problem With Offset Pagination at Scale ](#the-problem-with-offset-pagination-at-scale)
2. [  Cursor Pagination ](#cursor-pagination)
3. [  Lazy Collections ](#lazy-collections)
4. [  Chunked Iteration ](#chunked-iteration)
5. [  Choosing the Right Tool ](#choosing-the-right-tool)
6. [  Practical Gotchas ](#practical-gotchas)
7. [  Takeaways ](#takeaways)

  ![Cursor Pagination, Lazy Collections, and Chunked Iteration at Scale in Laravel](https://cdn.msaied.com/495/f4be5a97b9eba9d16cce791cca64bd80.png)

  #laravel   #eloquent   #performance   #pagination   #collections  

 Cursor Pagination, Lazy Collections, and Chunked Iteration at Scale in Laravel 
================================================================================

     1 Aug 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   The Problem With Offset Pagination at Scale  ](#the-problem-with-offset-pagination-at-scale)
2. [  02   Cursor Pagination  ](#cursor-pagination)
3. [  03   Lazy Collections  ](#lazy-collections)
4. [  04   Chunked Iteration  ](#chunked-iteration)
5. [  05   Choosing the Right Tool  ](#choosing-the-right-tool)
6. [  06   Practical Gotchas  ](#practical-gotchas)
7. [  07   Takeaways  ](#takeaways)

 The Problem With Offset Pagination at Scale
-------------------------------------------

Offset-based pagination (`LIMIT 100 OFFSET 50000`) forces the database to scan and discard 50,000 rows before returning your page. On a table with millions of rows and a busy write load, this degrades predictably. The query plan cannot use an index seek for the offset portion — it must materialise the skipped rows first.

Laravel gives you three better tools depending on your use case: **cursor pagination**, **lazy collections**, and **chunked iteration**. They are not interchangeable.

---

Cursor Pagination
-----------------

Cursor pagination encodes the last-seen value of an ordered column into an opaque token. The next page is fetched with a `WHERE id > :last_id` clause, which the database resolves with a single index seek.

```php
$users = User::orderBy('id')->cursorPaginate(50);

// In your API resource:
return [
    'data' => UserResource::collection($users),
    'next_cursor' => $users->nextCursor()?->encode(),
    'prev_cursor' => $users->previousCursor()?->encode(),
];

```

The generated SQL looks like:

```sql
SELECT * FROM users
WHERE id > 84302
ORDER BY id ASC
LIMIT 51; -- one extra row to detect the next page

```

**Constraints to know:**

- The sort column must be unique or combined with a tiebreaker (`orderBy('created_at')->orderBy('id')`).
- You cannot jump to an arbitrary page — cursors are sequential.
- Ideal for infinite scroll, feeds, and API consumers that process pages linearly.

---

Lazy Collections
----------------

`LazyCollection` wraps a PHP generator. Eloquent's `cursor()` method returns one, hydrating one model at a time from a forward-only database cursor.

```php
User::where('active', true)
    ->orderBy('id')
    ->cursor()
    ->each(function (User $user) {
        ProcessUser::dispatch($user);
    });

```

Memory stays flat because only one model is alive at a time. The database connection, however, stays open for the duration of the iteration. On long-running processes this can exhaust connection pool slots.

```php
// Combine with lazy() for collection pipeline operations without loading all rows
User::cursor()
    ->filter(fn (User $u) => $u->isEligible())
    ->map(fn (User $u) => $u->toExportArray())
    ->each(fn (array $row) => $csv->writeRow($row));

```

The `filter` and `map` here are lazy — they do not buffer the full result set.

**When to prefer `cursor()`:** transformation pipelines, exports, and read-only processing where you need collection methods but cannot afford to load everything into memory.

---

Chunked Iteration
-----------------

`chunk()` and `chunkById()` issue multiple queries, each fetching a fixed number of rows. The connection is released between chunks.

```php
// chunk() uses OFFSET — fine for small tables, degrades at scale
User::chunk(500, function (Collection $users) {
    foreach ($users as $user) {
        // ...
    }
});

// chunkById() uses WHERE id > :last_id — scales correctly
User::orderBy('id')->chunkById(500, function (Collection $users) {
    foreach ($users as $user) {
        // ...
    }
});

```

Always prefer `chunkById()`. It avoids the offset scan and is safe when rows are inserted or deleted mid-iteration because it anchors on the primary key, not a row offset.

```php
// Custom key column for tables without a sequential `id`
User::chunkById(500, function ($users) { /* ... */ }, column: 'uuid');

```

---

Choosing the Right Tool
-----------------------

| Scenario | Best fit | |---|---| | API with next/prev navigation | `cursorPaginate()` | | Export / ETL pipeline | `cursor()` + `LazyCollection` | | Background job processing all rows | `chunkById()` | | Small table, arbitrary page jumps | `paginate()` (offset is fine) |

---

Practical Gotchas
-----------------

- **Eager loading with `cursor()`:** `with()` is ignored — Eloquent cannot batch eager loads across a generator. Use `chunkById()` if you need relationships.
- **Transactions and `chunk()`:** wrapping a chunk callback in a transaction is safe; wrapping the entire `chunk()` call is not — it holds a transaction open across all chunks.
- **`lazyById()`:** a convenience wrapper that combines `chunkById()` with a `LazyCollection` facade, giving you collection methods without holding an open cursor.

```php
User::lazyById(500, column: 'id')
    ->filter(fn ($u) => $u->plan === 'pro')
    ->each(fn ($u) => SendRenewalReminder::dispatch($u));

```

---

Takeaways
---------

- Replace `paginate()` with `cursorPaginate()` for any API endpoint that processes pages sequentially.
- Use `cursor()` / `LazyCollection` for memory-sensitive pipelines, but watch open connection duration.
- Always use `chunkById()` over `chunk()` on large tables — offset scans are a silent performance killer.
- `lazyById()` is the pragmatic middle ground: chunked queries with a lazy collection interface.
- Eager loading requires `chunkById()`; `cursor()` cannot batch relationship queries.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fcursor-pagination-lazy-collections-and-chunked-iteration-at-scale-in-laravel-1&text=Cursor+Pagination%2C+Lazy+Collections%2C+and+Chunked+Iteration+at+Scale+in+Laravel) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fcursor-pagination-lazy-collections-and-chunked-iteration-at-scale-in-laravel-1) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Can I use eager loading with Laravel's cursor() method?        No. Eloquent's cursor() returns a generator that hydrates one model at a time, so it cannot batch eager load relationships. If you need relationships, use chunkById() instead, which loads a full collection per chunk and supports with(). 

      Q02  What is the difference between lazyById() and cursor() in Laravel?        cursor() opens a single forward-only database cursor and keeps the connection open for the full iteration. lazyById() issues multiple chunked queries under the hood but exposes a LazyCollection interface, releasing the connection between chunks. lazyById() is safer for long-running processes with connection pool constraints. 

      Q03  When should I still use offset-based paginate() instead of cursorPaginate()?        Use paginate() when users need to jump to an arbitrary page number (e.g. 'go to page 47') or when the dataset is small enough that offset scans are negligible. cursorPaginate() is strictly sequential and cannot support random page access. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![PhpStorm 2026.2 Released: Laravel Tool Window, PHP 8.5 Pipe Operator, and AI Agents](https://cdn.msaied.com/505/151a0bba66cc27064e090e69e55d7c92.png) PhpStorm JetBrains PHP 8.5 

### PhpStorm 2026.2 Released: Laravel Tool Window, PHP 8.5 Pipe Operator, and AI Agents

PhpStorm 2026.2 ships a dedicated Laravel tool window with Artisan, error logs, and Laravel Cloud tabs, plus P...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 3 Aug 2026     3 min read  

  Read    

 ](https://msaied.com/articles/phpstorm-20262-released-laravel-tool-window-php-85-pipe-operator-and-ai-agents) [ ![Laravel Doctor: Diagnose Your Laravel App With One Artisan Command](https://cdn.msaied.com/504/d72224689abc7b396bce187535008272.png) Laravel Artisan Health Checks 

### Laravel Doctor: Diagnose Your Laravel App With One Artisan Command

Laravel Doctor is a first-party package announced at Laracon US 2026 that adds an `artisan doctor` command to...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 3 Aug 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-doctor-diagnose-your-laravel-app-with-one-artisan-command) [ ![Livewire v4.3.5 Released: Fix for SFC Detection with PHP Attribute Array Arguments](https://cdn.msaied.com/503/9678ed8dbf5d7a6f4f19ca7694cf241b.png) Livewire Laravel PHP 

### Livewire v4.3.5 Released: Fix for SFC Detection with PHP Attribute Array Arguments

Livewire v4.3.5 ships a targeted bug fix for Single File Component (SFC) detection when PHP attributes contain...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 3 Aug 2026     2 min read  

  Read    

 ](https://msaied.com/articles/livewire-v435-released-fix-for-sfc-detection-with-php-attribute-array-arguments) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
