Allowed memory size exhausted: finding what actually ate the RAM
Raising memory_limit makes the message go away for about a week. Here is how to read the error properly, find the code that actually allocated the memory, and work out whether more RAM is a fix or a postponement.
PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes)
is one of the most misread errors in PHP. It looks like a capacity problem. Nine times out of ten it is a
code problem that capacity was hiding.
What the message actually tells you
There are two numbers in that line and they mean very different things.
- 134217728 bytes is your
memory_limit, in bytes. That one is 128M. This is a per-process ceiling, not a server total, and PHP enforces it itself. - tried to allocate 20480 bytes is the request that failed, which is the single most useful diagnostic in the message and the one everyone skips.
If the second number is small, say under a megabyte, nothing dramatic happened at the end. The process was already sitting at 127.9M and a routine 20KB allocation tipped it over. The culprit is upstream, in whatever accumulated those 127.9 megabytes, and the failing line is just the unlucky one.
If the second number is large, 50M or 200M or more, then one allocation really did try to take a huge bite, and the failing line is genuinely where to look. That is usually an image, a file read into a string, or a query result hydrated all at once.
Why the named file is usually innocent
PHP reports the file and line where the ceiling was hit. With a slow accumulation, that is wherever the next
allocation happened to land: a template partial, a logger, str_replace inside a helper. Developers
then "optimise" a perfectly reasonable function, ship it, and hit the same error two days later from a different
file.
The question to ask is never "what is wrong with this line". It is "what was already holding 128 megabytes by the time this line ran".
The four things that really cause it
1. A result set loaded whole
Fetching every row and hydrating each one into an object is the classic. Ten thousand rows of twenty columns is
nothing in the database and hundreds of megabytes in PHP once each row becomes a model instance with its own
relations. In WordPress this is get_posts() or WP_Query with
'posts_per_page' => -1, which is a loaded gun pointed at a site that grows.
2. Image processing
GD and Imagick decompress to raw bitmaps. A 2MB JPEG at 6000x4000 pixels is roughly 6000 x 4000 x 4 bytes, about 96MB, in memory, before you do anything to it. Two of those in one request and 128M is gone. This is why the error so often appears on upload or on a thumbnail regeneration run, and never during normal browsing.
3. Accumulation in a loop
An array that is appended to inside a loop that iterates over everything, a string concatenated in a loop, a log buffer that is never flushed. Memory grows linearly with data volume, so it works in development with 50 records and dies in production at 50,000. These are the ones that appear months after deploy with no code change at all.
4. Recursion or a reference cycle
This one is rarer, but when it does happen the growth is fast rather than linear. The usual shapes are an object graph that references itself, a recursive function whose termination condition is wrong for certain inputs, or an autoloader loop. Look here when the error is intermittent and tied to one specific record.
Finding the allocation, not guessing at it
Before changing any configuration, get a measurement. memory_get_peak_usage(true) returns the real
high-water mark in bytes, including PHP's own allocator overhead. Drop it at a few checkpoints and the growth
shows up immediately:
error_log(sprintf(
'[mem] %s: %.1f MB',
'after-query',
memory_get_peak_usage(true) / 1048576
));
Three or four of those through a request narrows the problem to one block of code in a single reload. It is cruder than a profiler and it works everywhere, including on shared hosting where you cannot install anything. If you can install something, Xdebug's memory profile or Blackfire will show you the exact call tree.
Two details that save time. Pass true to get the allocated size rather than the used size, because
the allocated figure is what PHP compares against the limit. And check the peak, not the current value, since
memory freed before the error still counted on the way up.
When raising the limit is correct
Sometimes it genuinely is. The honest test is whether the memory is bounded by the work or bounded by your data.
- Raise it when the peak is stable and the job legitimately needs that much: image processing, a PDF render, a one-off import. The number will not grow next month because the work is the same shape.
- Fix the code when the peak tracks the number of rows, files, or users. Raising the limit here buys you however long it takes the data to grow, which on a healthy site is weeks.
The practical compromise most teams land on: raise the limit so the site stops erroring tonight, open a ticket for
the real fix, and put a note on the config line saying which job needed it. An undocumented
memory_limit = 512M is a thing nobody will ever dare lower again.
WordPress, Laravel and CLI specifics
WordPress
WordPress has its own layer on top of php.ini. In wp-config.php, above the
"stop editing" line:
define('WP_MEMORY_LIMIT', '256M');
define('WP_MAX_MEMORY_LIMIT', '512M');
WP_MEMORY_LIMIT applies to the front end. WP_MAX_MEMORY_LIMIT applies to admin screens
and cron, which are where the heavy jobs live. Neither can exceed what the host allows, so if the values appear to
do nothing, your host has a hard cap and you need to raise it there or ask them to.
Laravel and Symfony
Framework bootstrapping is not usually the problem; collections are. Model::all() loads everything.
chunk() and cursor() exist precisely for this, and cursor() uses lazy
collections so memory stays flat regardless of row count. For queued jobs, remember the worker is a long-lived
process: memory that leaks per job accumulates across jobs until the worker is recycled.
The CLI has its own limit
This one catches people constantly. The CLI SAPI reads a different php.ini than FPM, and on many
distributions the CLI limit is already -1. So a script runs fine by hand and dies under cron or
through the web. Check with php -i | grep memory_limit for the CLI value and a
phpinfo() page for the web value, and expect them to differ. To override for one run:
php -d memory_limit=512M artisan import:run
Stopping the 3am repeat
Memory exhaustion is one of the most predictable production failures there is, because it almost always follows data growth rather than a deploy. That also makes it one of the easiest to catch early, if anything is actually watching the log.
Two things worth putting in place regardless of what tooling you use. First, make fatals visible: a
register_shutdown_function that checks error_get_last() for
E_ERROR and reports it somewhere you will see, because a fatal never reaches your exception
handler. Second, log the peak memory on slow or heavy endpoints, so the climb is visible as a trend rather than
as an outage.
The failure mode that costs real money is not the error itself. It is the eight hours between the site breaking and someone noticing, and that gap is what Patcherly exists to close. The error is caught as it happens, the fix is drafted and waiting, and you decide from your phone whether it applies.
Found something wrong in here? Tell me.