Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

fukuball's avatar

Laravel 5.8 memory leak

Demo Repo

https://github.com/fukuball/Leak58

Laravel memory leak example

It is normal to loop and process data in script, I found some weird memory leak in Laravel, and wonder how this happened. I have some workaround to prevent the memory leak, but it can't solve the root cause, so I provide some cases to demo the memory leak, hope someone can solve this issue.

Install

$ composer install
$ php artisan migrate

Data seeding

$ php artisan leak_test_data

Usage

Case 1

This first case demo a simple loop cause the memory leak:

$ php artisan leak_test leak

Some details:

$albums = Album::take(10000)->get();
foreach ($albums as $album) {
    $songs = $album->songs; // cause memory leak
}

And we can see the memory goes up and never come back:

...
550 start
#executions = 550 - mem: 14799640
550 end
551 start
#executions = 551 - mem: 14801728
551 end
552 start
#executions = 552 - mem: 14803800
552 end
553 start
#executions = 553 - mem: 14805872
553 end
554 start
#executions = 554 - mem: 14807944
554 end
555 start
#executions = 555 - mem: 14810024
555 end
556 start
#executions = 556 - mem: 14812112
556 end
557 start
#executions = 557 - mem: 14814192
557 end
...

I know there is N+1 query in it, but simple loop with simple qurey should not cause memory leak, it happend in Laravel.

Case 2

This second case demo a simple loop with N+1 query, but no memory leak:

$ php artisan leak_test no_leak

Some details:

$albums = Album::take(10000)->get();
foreach ($albums as $album) {
    $songs = $album->songs()->get(); // why this don't cause the memory leak?
}

We can see the memory usage is stable:

...
439 start
#executions = 439 - mem: 13659472
439 end
440 start
#executions = 440 - mem: 13659456
440 end
441 start
#executions = 441 - mem: 13659464
441 end
442 start
#executions = 442 - mem: 13659456
442 end
443 start
#executions = 443 - mem: 13659456
443 end
444 start
#executions = 444 - mem: 13659464
444 end
445 start
#executions = 445 - mem: 13659456
445 end
446 start
#executions = 446 - mem: 13659464
446 end
...

This is reasonable, although there is N+1 query, but should not cause memory leak.

Case 3

Third case demo a simple loop and use "with" to solve N+1 query.

$ php artisan leak_test leak_solve_by_with

Some details:

$albums = Album::with(['songs'])->take(10000)->get();
foreach ($albums as $album) {
    $songs = $album->songs; // cause memory leak
}

We can see the memory usage is always same:

...
1239 start
#executions = 1239 - mem: 16015944
1239 end
1240 start
#executions = 1240 - mem: 16015944
1240 end
1241 start
#executions = 1241 - mem: 16015944
1241 end
1242 start
#executions = 1242 - mem: 16015944
1242 end
1243 start
#executions = 1243 - mem: 16015944
1243 end
1244 start
#executions = 1244 - mem: 16015944
1244 end
1245 start
#executions = 1245 - mem: 16015944
1245 end
1246 start
#executions = 1246 - mem: 16015944
1246 end
1247 start
#executions = 1247 - mem: 16015944
1247 end
...

This is trivial, this solve the N+1 query, get all the data first, and Laravel use the data and no need to query again and again, so if we can get all the data into the memory, the script will excute perfectly.

Case 4

This case demo a common case when we write OOP, we use use some method in model, and model will get the necessary data to proceed the work.

$ php artisan leak_test leak_weird

Some details:

$albums = Album::take(10000)->get();
foreach ($albums as $album) {
    $songs = $album->processSomethingToReturn();
}

// in Album.php
public function processSomethingToReturn()
{
    $songs = $this->songs; // this cause memory leak
    // do something here...
    return $songs;
}

And we can see the memory goes up and never come back:

...
453 start
#executions = 453 - mem: 14598224
453 end
454 start
#executions = 454 - mem: 14600296
454 end
455 start
#executions = 455 - mem: 14602376
455 end
456 start
#executions = 456 - mem: 14604456
456 end
457 start
#executions = 457 - mem: 14606528
457 end
458 start
#executions = 458 - mem: 14608616
458 end
459 start
#executions = 459 - mem: 14610688
459 end
460 start
#executions = 460 - mem: 14612760
460 end
461 start
#executions = 461 - mem: 14614840
461 end
...

This is common to write some method for "Encapsulation", we don't need to know the detail, just call the method to do what we want. But in this case we got memory leak.

Case 5

In this final case we use walkaround to solve the memory leak by "with" magic:

$ php artisan leak_test leak_solve_by_with_weird

Some details:

$albums = Album::with(['songs'])->take(10000)->get();
foreach ($albums as $album) {
    $songs = $album->processSomethingToReturn();
}

// in Album.php
public function processSomethingToReturn()
{
    $songs = $this->songs; // this cause memory leak
    // do something here...
    return $songs;
}

We can see the memory usage is always the same:

...
1240 start
#executions = 1240 - mem: 16015952
1240 end
1241 start
#executions = 1241 - mem: 16015952
1241 end
1242 start
#executions = 1242 - mem: 16015952
1242 end
1243 start
#executions = 1243 - mem: 16015952
1243 end
1244 start
#executions = 1244 - mem: 16015952
1244 end
1245 start
#executions = 1245 - mem: 16015952
1245 end
1246 start
#executions = 1246 - mem: 16015952
1246 end
1247 start
#executions = 1247 - mem: 16015952
1247 end
1248 start
#executions = 1248 - mem: 16015952
1248 end
...

This walkaround solve the memory leak, but really wired, in "Encapsulation" principle, we shoud not to know the detail of method, so we use with(['songs']) in advence is really wired, this should not happend when we write code.

Apparently we shold solve the root cause of memory leak. Why $this->songs in loop cause memory leak but $this->songs()->get() not?

0 likes
22 replies
staudenmeir's avatar

I wouldn't call this a memory leak. Your application requires more memory the longer it runs because it has to store more variables.

Case 1 requires more memory because$songs = $album->songs stores the query result in the $album model. The more iterations your loop has, the more memory it requires.

Case 2 behaves differently because it only overwrites the value of $songs with the current query result. The previous results aren't stored anywhere. This keeps the memory usage stable.

Case 3 uses the same amount of memory as case 1 but requires it all at once in the first line.

click's avatar

Yes this doesn't look like a "leak". Try filling up an array in your second example instead of overwriting the $songs variable.

$albums = Album::take(10000)->get();
$songs = [];
foreach ($albums as $album) {
    $songs[] = $album->songs()->get(); // why this don't cause the memory leak?
}

it would behave the same way as your first case I suppose

fukuball's avatar

@staudenmeir @click I try to unset the variables every iteration, the memory still grows, is there any way to free the memory?

$albums = Album::take(10000)->get();
foreach ($albums as $album) {
    $songs = $album->songs; // cause memory leak
    unset($songs);
    unset($album); // can't free the memory, I think in this case is memory leak
}
staudenmeir's avatar

What does your actual code look like? What are you doing the loop?

click's avatar

note that when you unset() anything it does not instantly free up the memory. The garbage collector needs to pass by first.

Some SO references:

btw, what you are seeing is not a real "memory leak" it is just consuming memory for all the data you collect.

And I doubt if unsetting $songs is actually saving you memory because it is only a reference to the real collection object that is stored in the $album model. So if you want to clear that you should also clear the relation itself maybe $album->setRelation('songs', null); can do the trick.

fukuball's avatar

@staudenmeir @click Actual code looks like this: (also updated on GitHub) https://github.com/fukuball/Leak58

$albums = Album::take(10000)->get();

$i = 1;
foreach ($albums as $album) {
    $songs = $album->songs;
    echo "$album->id start \n";
    echo "#executions = " . $album->id . " - mem: " . memory_get_usage() . "\n";
    echo "$album->id end \n";
    $album->setRelation('songs', null);
    unset($songs);
    unset($album);
    $i++;
}

$album->setRelation('songs', null); still can do the trick, the memory goes up and never come back.

...
1261 start 
#executions = 1261 - mem: 14746184
1261 end 
1262 start 
#executions = 1262 - mem: 14746560
1262 end 
1263 start 
#executions = 1263 - mem: 14746936
1263 end 
1264 start 
#executions = 1264 - mem: 14747320
1264 end 
1265 start 
#executions = 1265 - mem: 14747696
1265 end 
1266 start 
#executions = 1266 - mem: 14748064
1266 end 
1267 start 
#executions = 1267 - mem: 14748456
1267 end 
1268 start 
#executions = 1268 - mem: 14748816
1268 end 
1269 start 
#executions = 1269 - mem: 14749200
1269 end
...
Cronix's avatar
$songs = $album->songs;

is that a relationship? If so, it's performing a query (in a loop). It would be better to eager load them, then there would only be a total of 2 queries instead of N+1. Also, every query gets logged in memory. Running them in a loop like that is not good. You can disable query logging.

fukuball's avatar

@cronix I know it's a N+1 query, but N+1 query should not cause the memory leak, I discuss by cases in the main article above.

Snapey's avatar

How about?

If you have a collection of $albums and then you foreach over those as $album, presumably $album is a reference to the member of the $albums collection?

When you load extra data into $album, actually you are expanding the member of $albums - memory which you never release.

When you execute ->get() you are perhaps creating new $song rather than expanding the content of the $albums collection?

fukuball's avatar

@snapey If the underlying mechanism is expanding the member of $albums, then we can explain why the memory grows, but it can't free the memory by $album->setRelation('songs', null);, I think it is a memory leak, do you think it's a memory leak?

Tray2's avatar

It's not a memory leak it's bad code plain and simple. You have 10000 records that you load all songs from into memory. It will use alot of memory that later on will be released when the query is finished. If you load 10 albums or even 100 in one go instead of 10000 you will use up a more reasonable amount of memory and it will be released much faster.

Simple calculation

One album = 1Kb One album contains aprox, 10 songs of 512b each

(10000 x 1024b) + (10000 x 512b x 10) = 9.7Mb + 48.8Mb = 58.5Mb

shez1983's avatar

if you need to process that many records:

  1. use chunking
  2. use RAW SQL (do the joins yourself to get relationships).
click's avatar

If the underlying mechanism is expanding the member of $albums, then we can explain why the memory grows, but it can't free the memory by $album->setRelation('songs', null);, I think it is a memory leak, do you think it's a memory leak?

@fukuball PHP's memory is not instantly cleared when you set a variable to null or use unset(). This is simply how PHP works. It is cleared when the garbage collector is passing by. You can try to call it manually: gc_collect_cycles()

Another approach is to execute your "memory heavy code" into a separate function as explained in an earlier link I've posted but also in a comment on the php doc site. This should automatically clean up the memory each time the function is finished. This only works if there are no other references to the variables.

// After testing, breaking up memory intensive code into a separate function allows the garbage collection to work.

For example the original code was like:-
while(true){
   //do memory intensive code
}

can be turned into something like:-
function intensive($parameters){
   //do memory intensive code
}

while(true){
   intensive($parameters);
}

// source: https://www.php.net/manual/en/features.gc.collecting-cycles.php#122544
click's avatar
click
Best Answer
Level 35

@fukuball I've created my own quick test and it does seem to work well here.

Output without unsetting the variables

Memory Used for 832 Items:         2.41 mb
    0       510.63 kb
   25         10.4 mb
   50         5.78 mb
   75          2.1 mb
  100         1.93 mb
  125         4.41 mb
  150          4.3 mb
  175         1.49 mb
  200         4.93 mb
  225         4.88 mb
  ....
  775         3.61 mb
  800         6.31 mb
  825         6.47 mb
Memory Used:        230.8 mb

When I clear the relation it gives me:

Memory Used for 832 Items:         2.41 mb
    0        142.5 kb
   25        32.38 kb
   50         32.7 kb
   75        36.95 kb
  100        29.38 kb
  125               0
  150               0
  175               0
  200               0
  225               0
...
  775               0
  800               0
  825               0
Memory Used:         2.64 mb

See the difference at the end: 2.6mb vs 230mb

Just a quick piece of code, you can run it in your artisan console as long as you set your model and you children. It shows the memory difference between the last 25 items.

function formatmem($m) {
    if ($m) {
        $unit = array('b', 'kb', 'mb', 'gb', 'tb', 'pb');
        $m = @round($m / pow(1024, ($i = floor(log($m, 1024)))), 2) . ' ' . $unit[$i];
    }
    return str_pad($m, 15, ' ', STR_PAD_LEFT);
}

$memoryStart = memory_get_usage();
$items = SomeModel::take(10000)->get();
$memoryAfterItems = memory_get_usage();
echo 'Memory Used for '. $items->count() .' items: '. formatmem($memoryAfterItems - $memoryStart) . PHP_EOL;

$prevMemory = $memoryAfterItems;
foreach ($items as $i => $item) {
    $records = $item->children;
    $records = null;
    unset($records);

    // $item->setRelations([]); // this one performs better as the one below for me
    // $item->setRelation('children', null);

    if ($i % 25 === 0) {
        $currentMemory = memory_get_usage();
        echo str_pad($i, 5, ' ', STR_PAD_LEFT) . ' ' . formatmem($currentMemory - $prevMemory) . PHP_EOL;
        $prevMemory = $currentMemory;
    }
}

$memoryEnd = memory_get_usage();
echo 'Memory Used: '. formatmem($memoryEnd - $memoryStart) . PHP_EOL;
spelcaster's avatar

Hi @fukuball, I had a similar problem and in my case laravel/telescope and ramsey/uuid were the culprits. Later this week I'll try to go deeper in this problem to give more details.

1 like
hotmeteor's avatar

@spelcaster Do you mind following up with some details here? We're trying to track a memory leak and use both of those packages.

click's avatar

@hotmeteor I did not use those specific packages used by @spelcaster but my issue was Sentry (Error logger), so probably similar behavior as Telescope.

My issue was:

This consumed memory

\DB::table('users')->get();

This did not:

$stmt = \DB::getPdo()->prepare('SELECT * FROM users');
$stmt->execute(); 
$stmt->fetchAll(); 

After a while I figured out my service provider for Sentry (pretty old version) was the culprit. After disabling it the leak was gone. What I did was instead of automatically including the service provider in config/app.php I only load it when necessary:

# AppServiceProvider::register() 
if ($conditionToLoadThis) {
    $this->app->register(SentryLaravelServiceProvider::class);
}

I don't know if this helps you out but try disabling all (custom) service providers you have and see if it has any effect.

If you have auto-discovery turned on (you probably have) than you can try disabling it via the dont-discover property in your composer.json: https://medium.com/@taylorotwell/package-auto-discovery-in-laravel-5-5-ea9e3ab20518

Please or to participate in this conversation.