Skip to content

Laravel error

Laravel: Allowed memory size exhausted

The error

Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes)

Raising the memory limit makes the error go away until the table grows. The code that needs it is almost always loading a whole result set into models when it could have streamed it.

The error

Symfony\Component\ErrorHandler\Error\FatalError

Allowed memory size of 134217728 bytes exhausted
(tried to allocate 20480 bytes)

The number in brackets is small and irrelevant - it is the allocation that happened to fail, not the one that filled the memory. Something before it consumed everything.

Where it comes from in a Laravel application

A full result set hydrated into models. The usual cause by a wide margin.

$orders = Order::all();                  // every row
$orders = Order::where(...)->get();      // every matching row

Each row becomes an Eloquent model - an object with attributes, original attributes for change tracking, relations and a connection reference. The overhead per row is far larger than the data, which is why a table that is a few hundred megabytes on disk will not fit in a gigabyte of memory as models.

A collection pipeline over that result set. ->get()->map()->filter() holds the source and builds new collections alongside it.

An export or import in one pass. Building a spreadsheet in memory, reading an uploaded file with file_get_contents, encoding a large array to JSON - each one holds the whole thing at once.

A queued job carrying too much. A job serialising a large collection rather than a set of ids means the payload is large in the queue and large again in the worker.

The fix, by shape of work

Reading a lot of rows. Do not hold them:

foreach (Order::where('status', 'pending')->cursor() as $order) {
    // one model at a time
}

cursor() keeps one database connection open for the traversal, which is fine for a command and worth thinking about inside a request.

Writing while iterating. Use the id-keyed variant, not the offset one:

Order::where('status', 'pending')->chunkById(500, function ($orders) {
    foreach ($orders as $order) {
        $order->update(['status' => 'processed']);
    }
});

chunk() pages with offset. Update the rows you are paging through and the pages shift, silently skipping records. chunkById() is the version that survives its own writes.

Aggregating. Ask the database rather than hydrating rows to count them:

// holds every row in memory
$total = Order::where('paid', true)->get()->sum('total');
 
// holds one number
$total = Order::where('paid', true)->sum('total');

Only needing a few columns. Hydration cost is per attribute as well as per row:

Order::select('id', 'total')->cursor();

Or skip models entirely where the work is genuinely a data pass:

DB::table('orders')->select('id', 'total')->cursor();   // arrays, not models

Exporting. Stream the response rather than building it:

return response()->streamDownload(function () {
    $out = fopen('php://output', 'w');
    foreach (Order::cursor() as $order) {
        fputcsv($out, [$order->id, $order->total]);
    }
    fclose($out);
}, 'orders.csv');

Jobs. Pass ids, not models or collections. The job reloads what it needs, in batches, on the worker.

When the limit genuinely is the problem

There are cases - a report that has to build a large structure, run nightly, where streaming is not possible. Raise the limit for that command rather than globally:

// inside the command
ini_set('memory_limit', '512M');

Raising it in php.ini for everything means a runaway request takes the whole process down instead of failing, and on a container it means the orchestrator kills the worker rather than PHP reporting an error you can read.

Finding it without guessing

The stack trace names the line that ran out, which is usually not the line that caused it. memory_get_peak_usage(true) logged at a few points around the suspect code narrows it faster than reading, and query logging shows whether a query you thought was limited is not.

The question that resolves most of these: how many rows does this return in production? Not in development, where the answer is always fine.

How many rows that query returns in production is a schema and indexing question as much as a PHP one, and a database engagement measures it on your data. Where the memory goes into models loaded one relation at a time, the N+1 is underneath.

Related questions

Is raising memory_limit ever the right answer?
Sometimes - a report that genuinely has to hold a large structure in memory, run once a night, is a fair case for a higher limit on that command alone. What is not fair is raising it globally because one job failed, because that hides the next twenty occurrences and turns a failure into an out-of-memory kill.
What is the difference between chunk and cursor?
chunk runs a separate query per batch and hydrates that batch into models. cursor runs one query and yields models one at a time, which is lighter for reading but holds a database connection open for the whole traversal. Use chunkById for anything that writes, cursor for a straight read.
Why did chunk skip records?
Because chunk pages with offset, and if the loop modifies the rows it is iterating - updating the column it orders by, or the one the where clause filters on - the pages shift underneath it. chunkById keys on the primary key instead, which is why it exists.
The error names a tiny allocation. Is that the cause?
No. The allocation in the message is simply the one that did not fit - it is the last straw rather than the weight. What matters is what was already in memory, which is almost never the thing being allocated when it failed.
Call us+1 848 272 7583WhatsApp+90 850 308 5436Emailinfo@codefacture.comContact page