PHP Allowed Memory Size Exhausted: Causes and Real Fixes

PHP allowed memory size exhausted means a script passed memory_limit. Learn the real causes and fix them with generators, chunking and unbuffered queries.

TL;DR

  • What: The fatal error means one PHP request tried to allocate more than the memory_limit setting allows.
  • Why it matters: Raising the limit hides the real bug, and the crash returns as soon as your data grows.
  • What to do: Measure peak usage, then stream data with generators, chunked queries or unbuffered cursors.
  • Key stat: The PHP manual says range(0, 1000000) uses well over 10 MB, while a generator needs under 1 KB.

The “Allowed memory size exhausted” fatal error means a PHP script tried to allocate more memory than the memory_limit directive permits, which defaults to 128M. Fix the cause by streaming data with generators, chunked queries or unbuffered cursors. Raise the limit only after measuring peak memory usage.

PHP allowed memory size exhausted is a fatal error raised by the Zend engine when a script’s allocations exceed the memory_limit INI directive. The limit applies per request or per CLI process, and it defaults to 128M. Setting it to -1 removes the limit entirely.

The message looks like Fatal error: Allowed memory size of 134217728 bytes exhausted, and it usually appears in production, not on your laptop. A seeded dev database holds 200 rows. The live table holds two million. The same get() or fetchAll() call that worked all quarter suddenly kills the request. Most guides tell you to edit php.ini and move on. That fix buys a few weeks. This post shows how PHP allowed memory size exhausted errors really happen, how to measure them, and how to fix the code so the error does not come back.

What does “PHP allowed memory size exhausted” actually mean?

It means the running script crossed the byte ceiling set by memory_limit, and the engine stopped it with a fatal error. The PHP manual lists the default as 128M. The directive can be set in php.ini, per directory, or at runtime with ini_set().

The manual describes the setting as a guard that stops “poorly written scripts” from eating all the memory on a server. That wording matters. The limit is a safety net, not a tuning knob.

One recent detail surprises people. The current manual says that in PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 the limit is a soft limit. An allocation that crosses it is, in most cases, allowed to finish before the fatal error fires. A separate memory_hard_limit_percent setting bounds how far usage can overshoot. So the number in the error message can be slightly lower than the real peak.

My recommendation: read the number in the message first. 134217728 bytes is exactly 128M. If you see 8388608, someone set 8M on purpose, and the bug may be the config, not the code.

Why does PHP allowed memory size exhausted happen with database queries?

Database reads are the most common cause, because MySQL results are buffered into PHP memory by default. The PHP manual on buffered queries states that buffered results are “immediately transferred from the MySQL Server to PHP and then are kept in the memory of the PHP process.”

That is convenient. It lets you count rows and run other queries while you loop. It also means a query for two million rows allocates two million rows, before your loop touches the first one.

Here is the pattern that fails in production:

<?php
// BROKEN: loads every row into memory at once
$rows = $pdo->query("SELECT id, email FROM users")->fetchAll(PDO::FETCH_ASSOC);

foreach ($rows as $row) {
    sendNewsletter($row['email']);
}
// Fatal error: Allowed memory size of 134217728 bytes exhausted

The same trap exists in frameworks. The Laravel documentation warns that an application “may run out of memory if you attempt to load tens of thousands of Eloquent records via the all or get methods.” Eloquent also builds a model object for every row, so it costs more than a raw array.

Does the memory limit count the result set?

It depends on the driver. The same manual page says that with libmysqlclient, the limit does not count result sets unless the data is fetched into PHP variables. With mysqlnd, “the memory accounted for will include the full result set.” Most modern PHP builds use mysqlnd, so assume the whole result counts.

How do you fix PHP allowed memory size exhausted with generators?

Use a generator to yield one row at a time, so memory stays flat no matter how many rows exist. The generators overview says a generator lets you feed a foreach loop “without having to build an array in memory ahead of time.”

The manual gives a concrete number. Calling range(0, 1000000) uses well over 10 MB, while the equivalent generator needs less than 1 KB for its own state. The cost is that you can only move forward. Random access is not possible.

Follow these steps to convert the broken loop above:

  1. Switch the connection to unbuffered mode with PDO::MYSQL_ATTR_USE_BUFFERED_QUERY set to false.
  2. Run the query with query() and do not call fetchAll().
  3. Wrap the loop in a generator function that yields each row.
  4. Consume the generator with foreach and do not store the rows in an array.
  5. Do not run a second query on the same connection until the loop finishes.
<?php
// FIXED: one row in memory at a time
function streamUsers(PDO $pdo): Generator
{
    $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
    $stmt = $pdo->query("SELECT id, email FROM users");

    foreach ($stmt as $row) {
        yield $row;
    }
}

foreach (streamUsers($pdo) as $row) {
    sendNewsletter($row['email']);
}

Step 5 is the catch. The manual states that “unless the full result set was fetched from the server no further queries can be sent over the same connection.” If your loop body writes to the database, open a second connection for the writes.

Unbuffered mode also shifts work to the server. The manual says it “uses less memory on the PHP-side, but can increase the load on the server.” Use it only for large, sequential reads, which is the case the manual recommends it for.

Which is better in Laravel: chunk, chunkById, lazy or cursor?

Use lazyById for most large jobs, because it keeps one chunk in memory and stays correct while you update rows. The Eloquent chunking docs describe five methods, and the differences are easy to miss.

Here is the part that most tutorials skip. The docs admit that cursor() “will still eventually run out of memory.” The reason is that PHP’s PDO driver internally caches all raw query results in its buffer. So cursor() saves model objects, but not the raw rows. For very large tables, the docs tell you to use lazy() instead.

Combine that with the unbuffered attribute from the previous section and you can make cursor() truly stream. I have not benchmarked that combination myself, so test it on a copy of your data first.

<?php
// Safe when you UPDATE the column you filter on
User::where('subscribed', true)
    ->lazyById(500, column: 'id')
    ->each(function (User $user) {
        $user->update(['digest_sent' => true]);
    });

Plain chunk() can skip rows when your update changes the filter column. The docs recommend chunkById or lazyById in that case. Doctrine users have a matching tool. The Doctrine batch processing guide covers bulk operations and warns that an enabled SQL logger “can have a serious impact on performance and resource usage” during batches.

How do you measure memory before raising memory_limit?

Call memory_get_peak_usage() at the end of the job and log the result. The PHP manual documents the signature as memory_get_peak_usage(bool $real_usage = false): int. By default it returns the memory used through PHP’s own allocator. With true it returns the real size reserved from the system.

<?php
register_shutdown_function(function () {
    $peakMb = memory_get_peak_usage(true) / 1024 / 1024;
    error_log(sprintf('[mem] peak=%.1f MB limit=%s', $peakMb, ini_get('memory_limit')));
});

Log this for a week before you touch the limit. Then compare peak against the limit across your slowest and heaviest endpoints. If one request type sits at 90% of the limit, fix that code path. If all of them sit at 20%, a single outlier is the problem.

Two other causes are worth ruling out. Circular references keep memory alive because plain reference counting cannot free them. PHP runs a cycle collector when its root buffer fills, and the manual says that buffer has a fixed size of 10,000 possible roots. Long-running workers that create many cyclic objects can call gc_collect_cycles() between jobs. Also keep in mind that each PHP-FPM worker holds its own memory, which is one reason long-lived streams are costly. I covered that effect in the SSE vs WebSockets guide.

When is raising memory_limit the right fix for PHP allowed memory size exhausted?

Raise it only when peak usage is legitimately high and bounded, such as image processing or a PDF export with a known maximum size. In that case set a specific value like 256M for that one script, not the whole server.

Avoid memory_limit = -1 in web requests. The manual confirms that -1 removes the limit, and then one runaway request can consume all RAM on the host. A related setting, max_memory_limit, caps what ini_set() may request. It defaults to -1 too, so set it deliberately if untrusted code can call ini_set().

A CLI job is different. A one-off migration run by an engineer can use php -d memory_limit=-1 script.php, because a human is watching it. Never copy that flag into a cron job or queue worker.

If the memory is high because you recompute a big aggregate on every request, caching is the cleaner fix. Just guard the rebuild properly, as in the cache stampede prevention guide.

How do the PHP memory-saving options compare?

Ways to handle large datasets in PHP and what each costs
Option Memory behaviour Complexity When to use
fetchAll() / get() Whole result set held in memory Lowest Small, bounded result sets only
chunk() / chunkById() One chunk at a time, one query per chunk Low Batch jobs; use chunkById if you update filtered columns
lazy() / lazyById() One chunk at a time, exposed as a single stream Low Most large Laravel jobs, with eager loading
cursor() One model at a time, but PDO still buffers raw rows Low Medium tables where eager loading is not needed
Unbuffered query + generator Rows stream from the server Medium Huge sequential reads on a dedicated connection
Raise memory_limit Same usage, higher ceiling Lowest Bounded, measured peaks in one script

Which fix should you pick?

The NexGismo verdict: Treat PHP allowed memory size exhausted as a design bug, not a config bug. Measure peak usage with memory_get_peak_usage(true), then switch large reads to lazyById() or an unbuffered generator. Raise memory_limit for one script at a time, and never set -1 on a web request.

  • The PHP memory_limit directive defaults to 128M, and setting it to -1 removes the limit.
  • In the PHP manual’s example, range(0, 1000000) uses well over 10 MB, while a generator version needs under 1 KB.
  • MySQL queries in PHP are buffered by default, so the whole result set sits in the PHP process until it is freed.
  • With mysqlnd, the PHP memory limit counts the full buffered result set.
  • Laravel’s cursor() still runs out of memory on huge tables, because PDO caches raw rows internally.
  • PHP’s cycle collector runs when its root buffer of 10,000 possible roots fills, and gc_collect_cycles() triggers it manually.

Frequently Asked Questions

How do I fix “Allowed memory size exhausted” in PHP?

Find which line allocates the memory, then stop loading everything at once. Replace fetchAll() or get() with a generator, chunked query or Laravel lazyById(). Measure peak usage with memory_get_peak_usage(true) first. Raise memory_limit only if the measured peak is legitimately high and bounded.

How do I increase the PHP memory limit?

Set memory_limit = 256M in php.ini, or call ini_set('memory_limit', '256M') at runtime for one script. The directive is changeable in php.ini, per directory and at runtime. Restart PHP-FPM or the web server after editing php.ini so the new value takes effect.

What is the default PHP memory_limit?

The default memory_limit is 128M, according to the PHP manual. Integer values are bytes, and shorthand like 128M or 1G is allowed. Your host or framework may override the default, so check the live value with ini_get('memory_limit') or phpinfo().

Is it safe to set memory_limit to -1?

It is safe only for supervised command-line jobs. A value of -1 removes the limit, so one runaway web request can use all RAM on the server. For web requests and queue workers, set a specific limit such as 256M and fix the code that needs more.

How do I find which PHP code uses the most memory?

Log memory_get_peak_usage(true) in a shutdown function, and compare it with ini_get('memory_limit'). Call memory_get_usage() before and after suspect blocks to see which line jumps. Profilers such as Xdebug or Blackfire can then show allocations per function.

What should you do next?

The PHP allowed memory size exhausted error is rarely about a small memory_limit. It is almost always about code that loads more data than it needs at once. Measure the peak, switch large reads to streams, and keep the limit as a safety net. That approach scales when your tables grow ten times. If you have a job that still hits the limit after these changes, send me the stack trace and I will look at it for a follow-up post.

Sources