Updated June 2026. Tested on Laravel 13 and PHP 8.4.

The moment you call get or all on a big table, Laravel loads every matching row into memory and builds a model for each one. On a few hundred rows that is fine. On hundreds of thousands, your process runs out of memory and dies. Laravel gives you a few ways to walk through a large table a bit at a time instead.

chunk

chunk pulls a fixed number of rows, hands them to your closure, then pulls the next batch, and so on. Only one batch is in memory at a time.

use App\Models\Product;

Product::chunk(200, function ($products) {
    foreach ($products as $product) {
        // process the product
    }
});

The first argument is the batch size. There is a catch worth knowing: if you are updating the rows as you go, and you update a column you are also filtering or ordering by, chunk can skip records. For that case use chunkById, which pages by the primary key and stays correct while you modify rows.

Product::where('is_active', true)->chunkById(200, function ($products) {
    foreach ($products as $product) {
        $product->update(['is_active' => false]);
    }
});

lazy

lazy gives you the same memory safety as chunk but with a nicer shape. It returns a LazyCollection you can loop directly, while it fetches in batches behind the scenes.

foreach (Product::lazy() as $product) {
    // process the product
}

This reads like a normal loop over every product, but it never holds more than one batch in memory. There is a lazyById too, with the same safety as chunkById when you are updating rows.

cursor

cursor goes further and runs a single query, keeping just one model in memory at a time using a PHP generator.

foreach (Product::where('is_active', true)->cursor() as $product) {
    // process the product
}

It is the lightest on memory, but because it is one long running query, it suits a straight read through the data. For most jobs lazy or chunkById is the balanced choice.

Which one to use

  • Reading through a large table: lazy.
  • Reading and updating as you go: chunkById or lazyById.
  • A single pass, lowest memory, pure read: cursor.

The rule of thumb is simple. If a table could ever grow large, never load it all at once. Stream it. Questions welcome in the comments.