Eloquent Chunking, Lazy Collections &amp; Cursor Pagination | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://msaied.com) - [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [About](https://msaied.com#about)

           [  Contact](https://msaied.com#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://msaied.com)
- [ProjectsCase studies](https://msaied.com/projects)
- [ArticlesEngineering notes](https://msaied.com/articles)
- [CertificatesCredentials](https://msaied.com/certificates)
- [AboutHow I work](https://msaied.com#about)
- [ContactGet in touch](https://msaied.com#contact)

  [Start a conversation](https://msaied.com#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://msaied.com)
2. /
3. [Articles](https://msaied.com/articles)
4. /
5. Eloquent at Scale: Chunked Iteration, Lazy Collections, and Cursor Pagination

 Eloquent at Scale: Chunked Iteration, Lazy Collections, and Cursor Pagination
==============================================================================

 Processing large Eloquent result sets without exhausting memory requires choosing the right tool. This article breaks down chunk(), lazy(), lazyById(), and cursor pagination so you can make informed decisions under real production load.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://msaied.com#person) Published 9 Oct 2026 · Updated 9 Oct 2026 · 4 min read

ShareCopy linkCopied

 ![Eloquent at Scale: Chunked Iteration, Lazy Collections, and Cursor Pagination](https://cdn.msaied.com/756/69efdf9ac9ec1e1f61378055f1cd6e92.png) 

  On this page +1. [The Problem With get() on Large Tables](#the-problem-with-codegetcode-on-large-tables)
2. [chunk(): Reliable but Stateful](#codechunkcode-reliable-but-stateful)
3. [lazy() and lazyById(): PHP Generators Over Eloquent](#codelazycode-and-codelazybyidcode-php-generators-over-eloquent)
4. [cursor(): True Server-Side Cursor](#codecursorcode-true-server-side-cursor)
5. [Cursor Pagination for APIs](#cursor-pagination-for-apis)
6. [Choosing the Right Tool](#choosing-the-right-tool)
7. [Key Takeaways](#key-takeaways)

 The Problem With `get()` on Large Tables
----------------------------------------

Calling `Model::all()` or `->get()` on a table with hundreds of thousands of rows loads every hydrated Eloquent model into memory simultaneously. On a 512 MB PHP worker this is a fast path to an OOM kill.

Laravel ships with several mechanisms to avoid this. Picking the wrong one for the job is a common source of subtle bugs and unnecessary database round-trips.

---

`chunk()`: Reliable but Stateful
--------------------------------

`chunk()` issues repeated `LIMIT / OFFSET` queries and hands each page to a closure.

```php
User::orderBy('id')->chunk(500, function (Collection $users) {
    foreach ($users as $user) {
        ProcessUser::dispatch($user);
    }
});

```

**Critical caveat:** if you mutate (update or delete) rows inside the closure and those rows affect the `ORDER BY` column, the offset shifts and you silently skip records. Use `chunkById()` instead — it re-anchors each page on the last seen primary key:

```php
User::chunkById(500, function (Collection $users) {
    $users->each(fn($u) => $u->update(['synced_at' => now()]));
});

```

`chunkById()` is safe for mutations and avoids the offset-drift bug. Prefer it by default.

---

`lazy()` and `lazyById()`: PHP Generators Over Eloquent
-------------------------------------------------------

Introduced in Laravel 8, `lazy()` wraps `chunkById()` in a PHP generator and returns a `LazyCollection`. You iterate a single stream of models without holding more than one chunk in memory at a time.

```php
User::where('active', true)
    ->lazy(200)
    ->each(function (User $user) {
        // Only 200 models hydrated at any moment
        $user->notify(new WeeklyDigest);
    });

```

Because `LazyCollection` is `Enumerable`, you can chain higher-order methods:

```php
User::lazy()
    ->filter(fn($u) => $u->isEligible())
    ->map(fn($u) => $u->toExportArray())
    ->each(fn($row) => $csv->writeRow($row));

```

Filter and map are evaluated lazily — no intermediate array is allocated.

`lazyById()` is the explicit variant that guarantees keyset pagination under the hood. Use it when you need to be explicit or when the default chunk size of `lazy()` (1000) is too large for your row width.

---

`cursor()`: True Server-Side Cursor
-----------------------------------

`cursor()` uses PDO's `PDO::FETCH_LAZY` to stream rows one at a time from a single query. No chunking, no repeated queries.

```php
foreach (User::cursor() as $user) {
    // One model in memory at a time
}

```

**Trade-offs:**

- The database connection is held open for the entire iteration.
- On MySQL with `mysqlnd`, the full result set is buffered client-side anyway unless you disable buffering — which requires careful PDO configuration.
- No safe mutation inside the loop (same offset-drift risk as `chunk()`).

For PostgreSQL, server-side cursors behave as expected. For MySQL in most Laravel setups, `lazyById()` is safer and often faster.

---

Cursor Pagination for APIs
--------------------------

For paginated HTTP responses, `cursorPaginate()` replaces numeric `OFFSET` with an opaque cursor encoding the last-seen keyset:

```php
// Controller
$users = User::orderBy('id')->cursorPaginate(25);
return UserResource::collection($users);

```

The JSON response includes `next_cursor` and `prev_cursor` tokens. This is O(log n) per page regardless of depth — critical when clients page deep into millions of rows.

```json
{
  "data": [...],
  "next_cursor": "eyJpZCI6MjUwMH0",
  "next_page_url": "/api/users?cursor=eyJpZCI6MjUwMH0"
}

```

Cursor pagination requires a stable, unique sort column. Composite cursors (e.g., `created_at` + `id`) are supported via multi-column `orderBy`.

---

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

| Scenario | Recommended API | |---|---| | Background job processing with mutations | `chunkById()` | | Read-only streaming with collection ops | `lazy()` / `lazyById()` | | Single-pass export, no mutations | `cursor()` (PostgreSQL) | | Deep-paginated REST API | `cursorPaginate()` |

---

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

- Never use `chunk()` with mutations on the sort column — use `chunkById()`.
- `lazy()` is `chunkById()` wrapped in a generator; prefer it for readable pipelines.
- `cursor()` holds an open connection and is only truly streaming on PostgreSQL.
- `cursorPaginate()` eliminates O(n) offset cost for API pagination at depth.
- Always add an index on the cursor column — keyset pagination is only fast with one.

- [laravel](https://msaied.com/articles?search=laravel)
- [eloquent](https://msaied.com/articles?search=eloquent)
- [performance](https://msaied.com/articles?search=performance)
- [database](https://msaied.com/articles?search=database)
- [collections](https://msaied.com/articles?search=collections)

 Frequently asked questions 
---------------------------

  What is the difference between lazy() and cursor() in Laravel Eloquent?lazy() issues multiple chunked queries using keyset pagination and returns a LazyCollection backed by a PHP generator. cursor() issues a single query and streams rows via PDO's lazy fetch mode. On MySQL, cursor() still buffers the full result client-side in most configurations, making lazy() the safer choice for large tables.

   When should I use cursorPaginate() instead of paginate() in a Laravel API?Use cursorPaginate() whenever clients may page deeply into a large result set. Standard paginate() uses OFFSET which degrades to O(n) as page number grows. cursorPaginate() uses a keyset cursor so each page fetch is O(log n) with a proper index, regardless of depth.

   Is it safe to update records inside a chunkById() callback?Yes. chunkById() re-anchors each page on the last seen primary key rather than using OFFSET, so updating or deleting rows within the callback does not cause records to be skipped or processed twice.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

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

[About](https://msaied.com#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articlePostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Mess](https://msaied.com/articles/postgresql-jsonb-in-laravel-indexing-querying-and-casting-without-the-mess)  

   On this page
-------------

1. [The Problem With get() on Large Tables](#the-problem-with-codegetcode-on-large-tables)
2. [chunk(): Reliable but Stateful](#codechunkcode-reliable-but-stateful)
3. [lazy() and lazyById(): PHP Generators Over Eloquent](#codelazycode-and-codelazybyidcode-php-generators-over-eloquent)
4. [cursor(): True Server-Side Cursor](#codecursorcode-true-server-side-cursor)
5. [Cursor Pagination for APIs](#cursor-pagination-for-apis)
6. [Choosing the Right Tool](#choosing-the-right-tool)
7. [Key Takeaways](#key-takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://msaied.com#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/755/2afbdc0a82de6bd61d3739c09554dda0.png)  · 4 min read### PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Mess

8 Oct 2026 ](https://msaied.com/articles/postgresql-jsonb-in-laravel-indexing-querying-and-casting-without-the-mess) [ ![](https://cdn.msaied.com/754/1ab77f05a0bd841971617d39adabd22e.png) Laravel · 3 min read### Laravel Fake Assertions Now Accept Property Arrays in Laravel 13.35

7 Oct 2026 ](https://msaied.com/articles/laravel-fake-assertions-now-accept-property-arrays-in-laravel-1335) [ ![](https://cdn.msaied.com/753/fea306145a8fc693561ccd02ff95afa5.png) Laravel · 3 min read### Filament v5.10.1 Released: Bug Fixes for Tables, Select Search, and Dropdown Navigation

7 Oct 2026 ](https://msaied.com/articles/filament-v5101-released-bug-fixes-for-tables-select-search-and-dropdown-navigation) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://msaied.com#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://msaied.com)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://msaied.com/sitemap.xml)
