Eloquent 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. [  Constraints you must respect ](#constraints-you-must-respect)
4. [  Lazy Collections: Streaming Results ](#lazy-collections-streaming-results)
5. [  When lazy collections bite you ](#when-lazy-collections-bite-you)
6. [  Chunked Iteration ](#chunked-iteration)
7. [  chunk() vs chunkById() ](#codechunkcode-vs-codechunkbyidcode)
8. [  Choosing the Right Tool ](#choosing-the-right-tool)
9. [  A Practical Export Example ](#a-practical-export-example)
10. [  Key Takeaways ](#key-takeaways)

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

  #laravel   #eloquent   #performance   #pagination   #collections  

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

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

       Table of contents

  10 sections  

1. [  01   The Problem with Offset Pagination at Scale  ](#the-problem-with-offset-pagination-at-scale)
2. [  02   Cursor Pagination  ](#cursor-pagination)
3. [  03   Constraints you must respect  ](#constraints-you-must-respect)
4. [  04   Lazy Collections: Streaming Results  ](#lazy-collections-streaming-results)
5. [  05   When lazy collections bite you  ](#when-lazy-collections-bite-you)
6. [  06   Chunked Iteration  ](#chunked-iteration)
7. [  07   chunk() vs chunkById()  ](#codechunkcode-vs-codechunkbyidcode)
8. [  08   Choosing the Right Tool  ](#choosing-the-right-tool)
9. [  09   A Practical Export Example  ](#a-practical-export-example)
10. [  10   Key Takeaways  ](#key-takeaways)

       The Problem with Offset Pagination at Scale
-------------------------------------------

Offset-based pagination (`LIMIT x OFFSET y`) is the default mental model for most Laravel developers. It works fine for the first few pages, but as `y` grows the database must scan and discard every preceding row before returning the requested slice. On a table with millions of rows, page 5000 is measurably slower than page 1 — and the gap widens with every index miss.

Laravel gives you three escape hatches: **cursor pagination**, **lazy collections**, and **chunked iteration**. Each solves a different problem.

---

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

Cursor pagination replaces the numeric offset with an opaque pointer derived from the last seen row's ordered column(s). The database seeks directly to that position instead of scanning.

```php
// Controller
$orders = Order::orderBy('id')
    ->cursorPaginate(50);

return OrderResource::collection($orders);

```

The response includes `next_cursor` and `prev_cursor` tokens. The client passes them back as `?cursor=eyJpZCI6NTB9`.

### Constraints you must respect

- The sort column(s) must be **unique** or combined with a unique tiebreaker (usually `id`). Without uniqueness, rows can appear on multiple pages or be skipped.
- You cannot jump to an arbitrary page number — cursors are sequential. If your UI needs "jump to page 47", cursor pagination is the wrong tool.
- Composite cursors work but require explicit column ordering:

```php
$orders = Order::orderBy('created_at')->orderBy('id')
    ->cursorPaginate(50);

```

Laravel encodes both columns into the cursor token automatically.

---

Lazy Collections: Streaming Results
-----------------------------------

`LazyCollection` wraps PHP generators to stream database rows one at a time without loading the full result set into memory.

```php
Order::where('status', 'pending')
    ->lazy()
    ->each(function (Order $order) {
        ProcessOrder::dispatch($order);
    });

```

Under the hood, `lazy()` issues a single query and uses a PHP generator to yield each hydrated model. Memory stays flat regardless of result count.

### When lazy collections bite you

Because the database cursor stays open for the duration of the iteration, long-running generators hold a connection for the entire loop. On a busy pool this can starve other requests. For truly long jobs, prefer `chunk()` instead.

Also, **eager loading does not compose with `lazy()`** the way you might expect:

```php
// This does NOT batch eager-load relationships
Order::with('items')->lazy(); // still N+1 per model

```

Use `lazyById()` with manual chunked eager loading, or switch to `chunk()`.

---

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

`chunk()` issues multiple bounded queries, each fetching a fixed number of rows. It releases the connection between batches, making it safe for long-running processes.

```php
Order::where('status', 'pending')
    ->chunkById(500, function (Collection $orders) {
        // Eager load inside the chunk — correct N+1 prevention
        $orders->load('items.product');

        foreach ($orders as $order) {
            ProcessOrder::dispatch($order);
        }
    });

```

### `chunk()` vs `chunkById()`

Always prefer `chunkById()` over `chunk()` when mutating rows inside the callback. Plain `chunk()` uses `OFFSET`, so deleting or updating rows mid-iteration shifts the window and causes skips. `chunkById()` uses a keyset approach (`WHERE id > ?`) which is stable under mutation.

---

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

| Scenario | Best fit | |---|---| | API list endpoint, sequential navigation | Cursor pagination | | Read-only streaming export / report | `lazy()` or `lazyById()` | | Background job processing with mutations | `chunkById()` | | Small dataset, arbitrary page jumps | Offset (`paginate()`) |

---

A Practical Export Example
--------------------------

```php
class ExportOrdersCsvJob implements ShouldQueue
{
    public function handle(): void
    {
        $stream = fopen('php://temp', 'r+');

        Order::query()
            ->select(['id', 'total', 'created_at'])
            ->chunkById(1000, function (Collection $orders) use ($stream) {
                foreach ($orders as $order) {
                    fputcsv($stream, [
                        $order->id,
                        $order->total,
                        $order->created_at->toIso8601String(),
                    ]);
                }
            });

        rewind($stream);
        Storage::put('exports/orders.csv', $stream);
        fclose($stream);
    }
}

```

Selecting only the columns you need keeps row hydration cheap and reduces network transfer from the database.

---

Key Takeaways
-------------

- Offset pagination degrades linearly; switch to cursor pagination for sequential API endpoints on large tables.
- `lazy()` streams rows with a single open cursor — great for reads, risky on long jobs with connection pools.
- `chunkById()` is the safest default for background processing, especially when rows are mutated during iteration.
- Always combine `chunkById()` with in-chunk eager loading to eliminate N+1 queries.
- Select only the columns you need — hydrating full models for a CSV export is wasteful.

 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&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) 

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

  3 questions  

     Q01  Can I use cursor pagination with complex WHERE clauses and multiple sort columns?        Yes. Laravel's cursor paginator encodes all ordered columns into the cursor token. The only hard requirement is that the combination of sort columns is unique across rows. Add `id` as a tiebreaker if your primary sort column (e.g. `created_at`) is not unique. 

      Q02  Why does `lazy()` still produce N+1 queries when I pass `with()` to it?        Because `lazy()` yields one model at a time via a PHP generator, there is no batch of models for Eloquent to eager-load against. Use `chunkById()` instead and call `$chunk-&gt;load('relation')` inside the callback to batch the eager load per chunk. 

      Q03  When should I still use plain offset `paginate()` instead of cursor pagination?        When your UI requires jumping to an arbitrary page number, or when the total count is needed for display. Cursor pagination cannot seek to page N without traversing all preceding cursors, and it does not expose a total row count. 

  Continue reading

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

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

 [ ![Job Batching, Chaining, and Rate-Limited Middleware in Laravel Queues](https://cdn.msaied.com/225/fc3ad6c9188459b1f2fb165912fca5b3.png) laravel queues jobs 

### Job Batching, Chaining, and Rate-Limited Middleware in Laravel Queues

Go beyond basic dispatching: learn how Laravel's Bus::batch(), job chains, and rate-limited middleware compose...

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

 17 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/job-batching-chaining-and-rate-limited-middleware-in-laravel-queues-1) [ ![Laravel Octane + FrankenPHP: Persistent State, Shared Services, and Safe Bootstrapping](https://cdn.msaied.com/224/cc0aa09965b63e7311e93282849ada05.png) laravel octane frankenphp 

### Laravel Octane + FrankenPHP: Persistent State, Shared Services, and Safe Bootstrapping

Running Laravel under FrankenPHP's worker mode unlocks real throughput gains, but persistent state between req...

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

 17 Jun 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-octane-frankenphp-persistent-state-shared-services-and-safe-bootstrapping) [ ![Laravel Contextual HTTP Clients: Per-Service Config, Retries, and Middleware Stacks](https://cdn.msaied.com/223/2706e56a293bb3b59f39b52956efac09.png) laravel http-client service-container 

### Laravel Contextual HTTP Clients: Per-Service Config, Retries, and Middleware Stacks

Stop scattering HTTP client config across service classes. Learn how to build named, pre-configured HTTP clien...

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

 17 Jun 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-contextual-http-clients-per-service-config-retries-and-middleware-stacks) 

   [  ![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)
