The error looks like this, and it always stops the script on the spot:
PHP Fatal error: Allowed memory size of 33554432 bytes exhausted (tried to allocate 33554440 bytes) in /var/www/app/report.php on line 3
The quick fix is to raise memory_limit. The better fix is to find out why the script needs that much memory, because in most cases it is holding something it could have processed piece by piece. Here is how to read the message, raise the limit properly, and cut the memory use itself.
Reading the message
-
Allowed memory size of 33554432 bytes is your
memory_limitin bytes. Divide by 1,048,576: this one is 32 MB. - tried to allocate 33554440 bytes is the single allocation that failed, not the total used. A big number here usually means one large array or string being created or grown. PHP arrays resize by doubling, so a loop that appends to an array fails at a request about the size of the array so far.
- in ... on line N is where the limit was hit, which is not always where the memory was wasted. The line that fails is just the one that asked for memory when none was left. The real cause is often earlier.
A tiny example that fails with a 32 MB limit:
$a = [];
for ($i = 0; $i < 10000000; $i++) {
$a[] = $i;
}
php -d memory_limit=32M a.php
That prints the exact error above, including the doubled allocation.
Step 1: Find the limit that actually applies
The CLI, PHP-FPM and Apache can each use a different php.ini, so the value you see in a terminal may not be the one your web request runs with.
php --ini # which php.ini the CLI loads
php -r 'echo ini_get("memory_limit");'
php-fpm8.3 -i | grep memory_limit # FPM's value (binary name varies by distro)
On the machine I tested, the CLI php.ini set memory_limit to -1 (unlimited), while PHP's built-in default, checked with php -n, is 128M. That is a common reason for "it works from the command line but fails on the web": the CLI has no limit and the web server does.
Step 2: Raise it, in the right place
| Where | How | Scope |
|---|---|---|
php.ini |
memory_limit = 256M |
Everything using that ini file. Restart FPM or Apache after changing it. |
| Command line | php -d memory_limit=512M script.php |
That one run |
| In the script | ini_set('memory_limit', '256M'); |
The current request or process |
| FPM pool file | php_admin_value[memory_limit] = 256M |
One pool |
ini_set() worked in my test: a script started with an 8 MB limit raised itself to 64 MB and then allocated a 30 MB string. Be aware that a value set with php_admin_value in the FPM pool configuration cannot be changed afterwards with ini_set(), according to the PHP manual, so if your ini_set seems to be ignored on the web, look at the pool file. I did not test FPM here.
Composer runs out of memory often and has its own switch:
COMPOSER_MEMORY_LIMIT=-1 composer update
# or
php -d memory_limit=-1 $(which composer) update
Avoid memory_limit = -1 for web requests. The limit is what stops one runaway request from taking down the whole server. Use a number, and size it from measurements.
Step 3: Use less memory
Raising the limit hides the problem until the data grows again. These are the usual patterns that use far more memory than needed. The numbers below are from PHP 8.3.6 on a 70 MB text file with 1.5 million lines:
| Approach | Peak memory |
|---|---|
$lines = file('big.txt'); |
216 MB (fails under a 64 MB limit) |
while (($line = fgets($h)) !== false) |
2 MB |
| Generator that yields one line at a time | 2 MB |
Note that the file is 70 MB but file() needs triple that, because every line becomes a separate PHP string with its own overhead. A generator keeps the code readable while holding one item at a time:
function lines(string $path): Generator
{
$h = fopen($path, 'r');
try {
while (($line = fgets($h)) !== false) {
yield $line;
}
} finally {
fclose($h);
}
}
foreach (lines('big.txt') as $line) {
// process one line
}
The same shape applies to data that is generated rather than read. With 2 million small values, building an array with iterator_to_array() peaked at 126 MB, while looping over the generator directly peaked at 2 MB. If you only need to loop once, do not collect first.
Database results (Laravel)
The most common source in Laravel apps is Model::all() or ->get() on a large table, which hydrates every row into a model object at once. Process in batches instead:
// Fixed-size batches, safe when you update rows while looping
User::query()->chunkById(1000, function ($users) {
foreach ($users as $user) {
// ...
}
});
// Same idea, one model at a time through a lazy collection
foreach (User::query()->lazyById(1000) as $user) {
// ...
}
I did not run these against a database for this article. They are documented Eloquent methods. Prefer chunkById or lazyById over chunk when the loop changes the column you filter on, because chunk uses offsets and can skip rows. If you only need a few columns, select() them. Exporting a whole table to CSV belongs in a queued job that streams, not in a request.
Step 4: Find what is using the memory
If it is not obvious, log the numbers when the limit is hit. A shutdown function still runs after this fatal error, so it can report where things stood:
register_shutdown_function(function () {
$e = error_get_last();
if ($e && str_contains($e['message'], 'Allowed memory size')) {
error_log(sprintf(
'OOM peak=%.1f MB at %s:%d',
memory_get_peak_usage(true) / 1048576,
basename($e['file']),
$e['line']
));
}
});
In my test this printed peak=16.0 MB and the failing line for a script limited to 16 MB. For finding the cause, call memory_get_usage() before and after suspicious steps and see which one jumps. A profiler such as Xdebug or Blackfire gives a full picture when that is not enough, though I did not use one for this article.
Things worth checking first:
- Queries without a limit, and results collected into an array "to process later".
-
file_get_contents()orfile()on user uploads or exports. - Image processing: the decoded bitmap in memory is far larger than the file on disk, since its size follows the pixel dimensions, not the file size.
- An infinite loop or runaway recursion that keeps adding data. If the usage grows forever whatever limit you set, this is it.
- Long-running processes such as queue workers, where leaks accumulate across jobs. Laravel's
queue:workhas a--memoryoption (128 MB by default) that makes the worker exit and restart when it grows too large.
Sources
- PHP Manual: memory_limit
- PHP Manual: Generators overview
- PHP Manual: memory_get_usage()
- Composer: Troubleshooting, memory limit errors
- Laravel Documentation: Eloquent, chunking results
Originally published on DEV Talk.
Top comments (1)
Raising the memory limit can be a useful diagnostic, but it should not be the final fix. Capturing peak usage by request shape and retaining a small allocation profile makes it possible to separate a legitimate workload increase from an unbounded collection, recursive path, or accidental full-table read.