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 rowEach 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 modelsExporting. 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.
