Fix: PHP Fatal error: Allowed memory size of X bytes exhausted
Part of: PHP, Ruby & Other Languages
Quick Answer
How to fix PHP Fatal error Allowed memory size exhausted caused by memory limits, large datasets, memory leaks, recursive functions, and inefficient queries.
Allowed memory size exhausted
The first time I hit this in production I did what everyone does, doubled memory_limit and moved on, and two weeks later the same endpoint fell over again at the new ceiling. I learned that the limit is almost never the problem; the problem is a query or a file read that scales with data you do not control, an export that was fine at 10,000 rows and fatal at 800,000. Raising the limit buys time, and sometimes that is the right immediate move, but in my experience the durable fix is always the same shape: stop holding the whole dataset in memory at once and stream it, chunk it, or push it to a queue.
Your PHP application crashes with:
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 65536 bytes)
in /var/www/html/app/process.php on line 42Or variations:
PHP Fatal error: Out of memory (allocated 268435456) (tried to allocate 4096 bytes)Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes)
in /vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php on line 588Composer: proc_open(): fork failed - Cannot allocate memoryPHP tried to use more memory than the configured limit allows. The default limit is typically 128MB (134217728 bytes). Your script needs more memory than is available.
One distinction worth making before you touch anything: “Allowed memory size of X exhausted” and “Out of memory (allocated X)” are different failures. The first means your script hit PHP’s own memory_limit, the fixes below apply. The second means PHP asked the operating system for more memory and the OS refused, the machine or container itself is out, and raising memory_limit cannot help. For that one (and for proc_open(): fork failed), look at container memory limits, other processes on the box, and swap, see the last section.
Where the Memory Actually Goes
PHP has a per-script memory limit (memory_limit in php.ini) that prevents a single script from consuming all server memory. When a script exceeds this limit, PHP kills it with a fatal error. The limit applies per-request in FPM and per-invocation in CLI, so a leak in one request does not directly impact another, but the FPM worker that handled it may exit and need replacement, which has its own cost.
The default memory_limit is 128 MB in PHP 7.x and 8.x. That value made sense in the WordPress-era web, when a typical page rendered a few KB of content and made a handful of database queries. Modern PHP applications, Laravel admin panels with large Eloquent collections, Symfony API responses serialized from rich entity graphs, image pipelines, CSV importers, routinely exceed it. The default is a safety net for shared hosting, not a target for production workloads.
A useful mental model: PHP scripts allocate memory in three places. First, the PHP runtime itself (opcode caches, autoloader state, framework bootstrapping). Second, your application objects (Eloquent models, Doctrine entities, parsed JSON). Third, extension memory (GD image buffers, intl collation tables, OPcache strings). The memory_limit covers the second and third categories per request. When the limit is hit, the allocation that pushed you over the edge is named in the error, but the actual culprit is usually whatever loaded too much data before that point.
Common causes:
- Loading too much data at once. Fetching millions of database rows into memory.
- Large file processing. Reading an entire large file into a string.
- Memory leaks. Objects accumulate in loops and are never freed.
- Recursive functions. Deep recursion with large data at each level.
- Image processing. GD or Imagick operations on large images.
- Composer installation. Composer itself needs memory for dependency resolution.
- Low memory limit. The default 128MB is too low for some operations.
- Eager-loaded relations.
with()on an Eloquent query that pulls in millions of related rows.
In Production: Incident Lens
In production, this error is almost always traffic-driven. A code path that runs fine on a small payload (single user, one CSV row, a 200 KB image) starves the worker when a real user uploads a 50 MB CSV or a 12-megapixel photo. The error is local to one request, but the secondary effects on PHP-FPM are what turn it into an incident.
How it surfaces: the first signal is an error spike in your monitoring, Sentry or your log aggregator shows multiple Allowed memory size of X bytes exhausted events within a minute. The next signal is FPM saturation: when many requests simultaneously hit a memory-heavy path, every active worker is occupied, and the rest of your traffic queues. Users see slow responses or 502/504 errors from the reverse proxy because Nginx ran out of available FPM upstream workers. The PHP error log fills with fatal-error lines, but the user-visible symptom is “the whole site is slow”, not “one endpoint failed.”
Blast radius: scoped per FPM pool. If you have a single pool serving everything, a memory-heavy request path can degrade the entire application. If you separated pools by concern (one pool for the web UI, one for the upload endpoint, one for cron), the blast is contained to the affected pool. The math matters: with pm.max_children = 16 and one heavy request taking 200 MB and 30 seconds, you have only 15 slots left for everything else. Five concurrent heavy uploads and the rest of the site stops responding.
Monitoring signal: the layered alerts are pool-level active workers, FPM listen queue depth, and per-request peak memory. Expose php-fpm status via pm.status_path = /status and scrape it with Prometheus or your APM. Alert when active workers exceed 80% of pm.max_children for more than 60 seconds, that is your saturation alarm and it triggers before users notice. Also alert on a sustained slow_log rate above baseline, since memory-heavy requests are often slow before they fail. Sentry’s memory_peak_usage() instrumentation lets you alert on the actual culprit endpoint rather than just the symptom.
Recovery sequence: the immediate move is to restart the affected FPM pool, systemctl restart php8.3-fpm releases stuck workers and clears the queue. If you can identify the offending request from the access log, block its source at the WAF or rate-limit the endpoint while you patch. Bump memory_limit for the specific pool (not globally) as a holding action, then deploy the real fix: streaming, chunking, or moving the heavy work to a queue. Never set memory_limit = -1 in production; you trade a fatal error for an OOM kill that takes the whole machine down.
Postmortem preventive: the durable fixes are operational, not just code. Split FPM pools by workload, a separate pool for uploads, exports, image processing, with its own memory_limit and pm.max_children. Move every “heavy” code path to a job queue (Laravel Horizon, Symfony Messenger) so the web tier returns quickly and the worker pool can be sized for memory rather than concurrency. Add pm.max_requests = 500 so workers are recycled before slow leaks add up. Stream files (fopen + fgets, generators, SplFileObject) rather than reading them whole; chunk database iteration (chunk(), cursor()); avoid with() on unbounded relations. Set up CI tests that import a representative-large fixture file and assert peak memory stays below a threshold.
Fix 1: Increase the Memory Limit
The quickest fix. Increase memory_limit in PHP configuration:
In php.ini:
; Find your php.ini:
; php -i | grep php.ini
; Common locations: /etc/php/8.3/fpm/php.ini, /etc/php/8.3/cli/php.ini
memory_limit = 256M
; or
memory_limit = 512M
; or for unlimited (not recommended in production):
memory_limit = -1Restart PHP-FPM after changing:
sudo systemctl restart php8.3-fpm
# or
sudo systemctl restart php-fpmIn a specific script (runtime override):
ini_set('memory_limit', '512M');In .htaccess (Apache):
php_value memory_limit 256MIn Docker (official php images):
The official php:8.3-fpm image does not read a PHP_MEMORY_LIMIT environment variable, that convention belongs to third-party images like webdevops or Matomo, and copying it onto the official image silently does nothing. Drop an ini file into the image’s conf.d directory instead:
# Dockerfile
FROM php:8.3-fpm
RUN echo 'memory_limit = 256M' > /usr/local/etc/php/conf.d/zz-memory.iniOr mount one from docker-compose.yml:
services:
php:
image: php:8.3-fpm
volumes:
- ./php-memory.ini:/usr/local/etc/php/conf.d/zz-memory.ini:roFor Composer specifically:
COMPOSER_MEMORY_LIMIT=-1 composer install
# or
php -d memory_limit=-1 /usr/local/bin/composer installMy rule for the limit: raise it to what the workload measurably needs, memory_get_peak_usage() tells you, and never to -1 in production. Unlimited memory does not remove the failure, it relocates it: instead of PHP killing one script with a readable fatal error, the kernel’s OOM killer eventually takes down the whole FPM pool, or the box. A bounded crash you can grep for beats an unbounded one you reconstruct from dmesg.
Fix 2: Process Data in Chunks
Loading all data at once is the most common cause. Process in batches:
Broken, loading all rows:
// Loads ALL users into memory at once
$users = User::all(); // 1 million rows = crash!
foreach ($users as $user) {
processUser($user);
}Fixed, use chunking:
// Laravel: Process 1000 rows at a time
User::chunk(1000, function ($users) {
foreach ($users as $user) {
processUser($user);
}
});
// Laravel: Lazy collection (even more memory-efficient)
User::lazy()->each(function ($user) {
processUser($user);
});Raw PDO with cursor:
$stmt = $pdo->prepare("SELECT * FROM users");
$stmt->execute();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
processUser($row);
// Only one row in memory at a time
}MySQLi unbuffered query:
$mysqli->real_query("SELECT * FROM large_table");
$result = $mysqli->use_result(); // Unbuffered — rows fetched one at a time
while ($row = $result->fetch_assoc()) {
processRow($row);
}
$result->free();The pattern I flag in every code review is ->get() or ::all() on a table whose size the code does not control. Both load the entire result set into hydrated model objects at once, and they work perfectly in development where the table has fifty rows. Reach for chunk(), lazy(), or cursor() whenever the row count is unbounded, and treat a bare all() as a bet that the table stays small forever.
Fix 3: Fix File Processing
Reading large files entirely into memory:
Broken:
// Loads the entire file into memory
$content = file_get_contents('/path/to/huge-file.csv'); // 2GB file = crash!
$lines = explode("\n", $content);Fixed, read line by line:
$handle = fopen('/path/to/huge-file.csv', 'r');
if ($handle) {
while (($line = fgets($handle)) !== false) {
processLine($line);
}
fclose($handle);
}Fixed, use SplFileObject:
$file = new SplFileObject('/path/to/huge-file.csv');
$file->setFlags(SplFileObject::READ_CSV);
foreach ($file as $row) {
if ($row[0] !== null) { // Skip empty lines
processRow($row);
}
}Fixed, use generators:
function readCsv(string $path): Generator {
$handle = fopen($path, 'r');
while (($row = fgetcsv($handle)) !== false) {
yield $row;
}
fclose($handle);
}
foreach (readCsv('/path/to/huge-file.csv') as $row) {
processRow($row);
// Only one row in memory at a time
}For JSON files:
// Wrong — loads entire JSON into memory
$data = json_decode(file_get_contents('huge.json'), true);
// Fixed — use a streaming JSON parser
// composer require halaxa/json-machine
use JsonMachine\Items;
$items = Items::fromFile('huge.json');
foreach ($items as $item) {
processItem($item);
}Fix 4: Fix Memory Leaks in Loops
Variables accumulating in loops:
Broken:
$results = [];
foreach ($largeDataSet as $item) {
$processed = heavyProcessing($item);
$results[] = $processed; // Array grows until memory runs out
log($processed); // If you only need to log, don't store
}Fixed, process and discard:
foreach ($largeDataSet as $item) {
$processed = heavyProcessing($item);
log($processed);
// Don't accumulate results if you don't need them all
}Fixed, write to file or database instead of storing in memory:
$outputFile = fopen('results.csv', 'w');
foreach ($largeDataSet as $item) {
$processed = heavyProcessing($item);
fputcsv($outputFile, $processed);
}
fclose($outputFile);Unset large variables when done:
$bigData = loadData();
processData($bigData);
unset($bigData); // Free the memory immediately
gc_collect_cycles(); // Force garbage collectionLaravel Eloquent in loops, disable event listeners and relations:
// Prevent Eloquent from accumulating query log
DB::disableQueryLog();
// Process in chunks
User::chunk(500, function ($users) {
foreach ($users as $user) {
$user->process();
}
});Fix 5: Fix Image Processing
Image operations can use massive amounts of memory:
// A 5000x5000 pixel image at 32-bit color uses ~100MB of raw memory
$image = imagecreatefromjpeg('large-photo.jpg');Estimate memory needed:
function estimateImageMemory(string $path): int {
[$width, $height] = getimagesize($path);
// 4 bytes per pixel (RGBA) + overhead
return (int) ($width * $height * 4 * 1.5); // 1.5x safety factor
}
// ini_get('memory_limit') returns shorthand like "128M", "1G", or "-1".
// Don't just intval() it: intval('1G') is 1 (about 1 MB after the multiply),
// and -1 means unlimited, both silently wreck the comparison.
function memoryLimitBytes(): int {
$v = ini_get('memory_limit');
if ($v === '-1') return PHP_INT_MAX; // unlimited
$n = (int) $v;
$unit = strtoupper(substr($v, -1));
if ($unit === 'G') return $n * 1024 ** 3;
if ($unit === 'M') return $n * 1024 ** 2;
if ($unit === 'K') return $n * 1024;
return $n; // plain bytes
}
$needed = estimateImageMemory('photo.jpg');
$available = memoryLimitBytes();
if ($needed > $available * 0.8) {
ini_set('memory_limit', ceil($needed / 1024 / 1024 * 2) . 'M');
}Use ImageMagick CLI instead of GD for large images:
// Process with ImageMagick command line — doesn't use PHP memory
exec('convert large-photo.jpg -resize 800x600 thumbnail.jpg');Resize before processing:
// Process in tiles or reduce resolution first
$image = imagecreatefromjpeg('large-photo.jpg');
$thumb = imagescale($image, 800, 600);
imagedestroy($image); // Free the original immediately
// Work with the smaller $thumbFix 6: Monitor Memory Usage
Track memory usage to find the problem:
echo "Memory: " . memory_get_usage(true) / 1024 / 1024 . " MB\n";
echo "Peak: " . memory_get_peak_usage(true) / 1024 / 1024 . " MB\n";Profile memory usage in sections:
function memoryCheckpoint(string $label): void {
static $last = 0;
$current = memory_get_usage(true);
$diff = $current - $last;
echo sprintf("[%s] Memory: %.2f MB (Δ %.2f MB)\n",
$label,
$current / 1024 / 1024,
$diff / 1024 / 1024
);
$last = $current;
}
memoryCheckpoint("Start");
$data = loadData();
memoryCheckpoint("After loadData");
processData($data);
memoryCheckpoint("After processData");Use Xdebug profiler for detailed analysis:
; php.ini
xdebug.mode=profile
xdebug.output_dir=/tmp/xdebugFix 7: Fix Recursive Functions
Deep recursion with large data structures:
Broken:
function processTree(array $node): array {
$result = processNode($node);
foreach ($node['children'] ?? [] as $child) {
$result['children'][] = processTree($child); // Recursive, accumulates memory
}
return $result;
}Fixed, use an iterative approach (mind the reference traps):
function processTree(array $root): array {
$stack = [];
$stack[] = &$root; // reference the real tree, not a copy
while ($stack) {
end($stack);
$key = key($stack);
$node = &$stack[$key]; // alias the actual element
unset($stack[$key]);
$node = processNode($node); // transform in place through the alias
if (!empty($node['children'])) {
// Push references to the REAL child elements. Iterating
// `$node['children'] ?? []` by reference would bind to a
// temporary copy and every mutation would be silently lost.
foreach (array_keys($node['children']) as $i) {
$stack[] = &$node['children'][$i];
}
}
unset($node); // break the alias before the next loop
}
return $root;
}PHP’s array references make this easy to get wrong: a naive version that pops with array_pop() and iterates a ?? [] fallback by reference runs without any error and returns the tree completely unprocessed. If you go iterative, verify with a small fixture that the transformations actually landed.
Fix 8: Optimize PHP-FPM Settings
PHP-FPM worker processes each consume memory:
; /etc/php/8.3/fpm/pool.d/www.conf
; Calculate: available_memory / memory_per_process = max_children
; Example: 4GB server, 256MB per process → max 16 children
pm = dynamic
pm.max_children = 16
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 8
; Restart workers after N requests to prevent memory leaks
pm.max_requests = 500Check per-worker memory usage:
ps aux --sort -rss | grep php-fpm | head -10Memory Exhaustion With Less Obvious Sources
Check for OPcache settings. OPcache uses shared memory separately from memory_limit:
opcache.memory_consumption=128
opcache.interned_strings_buffer=16Check for session storage issues. Large session data stored in memory can accumulate.
Check for third-party library leaks. Some libraries accumulate internal state. Check their documentation for cleanup methods.
Consider using a queue for heavy processing:
// Instead of processing in a web request:
dispatch(new ProcessLargeDataJob($dataId));
// The job runs in a separate process with its own memory limit
// php artisan queue:work --memory=512Check for realpath_cache_size exhaustion. When you load thousands of files (a large Composer autoloader, many includes), the realpath cache fills and PHP starts re-resolving paths repeatedly. This is not part of memory_limit but it correlates with memory pressure. Bump realpath_cache_size = 4096K and realpath_cache_ttl = 600 in php.ini.
Check for serialized session data growth. If you store large objects in $_SESSION and the session handler is file-based, each request reads the entire serialized blob into memory. Move large session payloads to a dedicated store (Redis, database) and keep only a session ID in the cookie.
Check for a template engine constructed inside a loop. Twig and Blade cache compiled templates automatically, so rendering the same template repeatedly through one engine instance is cheap. The memory trap is code that builds a fresh engine per iteration (new Twig\Environment(...) inside the loop, or resolving a new container per item), each instance carries its own compiled-template cache and none of it is shared. Construct the engine once outside the loop and reuse it; also avoid concatenating every rendered string into one giant buffer when you can stream output as you go.
Check for Doctrine entity manager retention. Doctrine’s identity map holds every entity loaded in the current request. In a long-running batch, call EntityManager::clear() periodically (every 100-500 entities) to release them.
proc_open(): fork failed - Cannot allocate memory means the OS, not PHP, is out. This shows up when Composer (or anything spawning subprocesses) forks on a small VPS with no swap: the fork momentarily needs to duplicate the parent’s memory footprint, and the kernel refuses. COMPOSER_MEMORY_LIMIT will not fix it. Add swap and retry:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
composer installFor Composer memory issues specifically, see Fix: PHP Composer memory limit. For similar memory patterns in JavaScript, see Fix: JavaScript heap out of memory. For container-level OOM events that look like memory errors, see Fix: Docker container exited 137 (OOMKilled). For queue worker memory tuning, see Fix: Laravel queue job not processing.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Laravel Queue Job Not Processing — Jobs Stuck in Queue
How to fix Laravel queue jobs not running — queue worker not started, wrong connection config, failed jobs, job timeouts, horizon setup, and database vs Redis queue differences.
Fix: PHP Session Not Working — $_SESSION Variables Lost Between Requests
How to fix PHP session variables that don't persist between requests — session_start() placement, cookie settings, session storage, shared hosting, and session fixation security.
Fix: PHP Warning: Undefined array key (Undefined index)
Fix the PHP warning Undefined array key, Undefined index, and Trying to access array offset on null by checking keys, using the null coalescing operator, and handling PHP 8 strictness.
Fix: Error: spawn ENOENT (Node.js child_process)
Fix Node.js 'Error: spawn ENOENT' from child_process.spawn(): PATH resolution, the Windows .cmd/.bat problem, and why shell: true is now discouraged (DEP0190).