If you ship PHP for a living, you will meet this fatal error sooner or later:
PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 4096 bytes) in /var/www/app/Services/ReportBuilder.php on line 88
In Laravel, the "allowed memory size exhausted" error usually hides inside storage/logs/laravel.log while your users just see a 500 page. In WordPress it is a white screen of death, or a 500 on admin-ajax during a backup, import, or page-builder save. In Composer it kills dependency installs mid-resolution. The error means one PHP process asked for more RAM than its memory_limit allows — and the fix is almost never just "raise the number and hope". Here is the method I use on client projects to kill the "allowed memory size exhausted" error for good: find the real limit, raise it in the right place for your runtime, then fix the code that blows through it.
What "Allowed Memory Size Exhausted" Actually Means
PHP gives every single process its own memory ceiling, set by the memory_limit directive. The default is 128M. When a script's allocations cross that line, PHP kills it immediately with a fatal error — there is no catching it, no retry, no graceful fallback. The "tried to allocate X bytes" part tells you how much the script wanted at the moment of death, and the file/line points at the allocation that pushed it over — which is often not the real culprit, just the last straw.
Two important truths shape every fix below.
First, PHP has several different runtimes on one machine, and each reads its own config. Your web app (PHP-FPM) and your command line (CLI) often use completely separate php.ini files. Raising the limit for the web SAPI does nothing for a Composer install or an Artisan command, and vice versa. Most "I raised it but it still fails" stories come from editing the wrong file.
Second, raising the limit treats the symptom. Legitimate workloads — big imports, report generation, image pipelines — sometimes do need more headroom. But an unbounded query, an infinite recursion, or a queue worker leaking a little on every job will eat any limit you give it. Give a leak 1GB and it dies at 1GB instead of 128MB, at 3 a.m. instead of 2 p.m. The durable fix is almost always in the code.
Step 1: Find the Limit You Are Actually Hitting
Before changing anything, confirm the current limit for the runtime that failed:
php -i | grep -i "memory_limit"
For a web request, drop this into a scratch route or script (never leave it on production):
<?php
echo 'memory_limit = ' . ini_get('memory_limit') . PHP_EOL;
echo 'peak so far = ' . round(memory_get_peak_usage(true) / 1024 / 1024, 2) . ' MB' . PHP_EOL;
If php -i says 512M but your Laravel queue job still dies at 128M, you are probably reading the CLI config while the worker runs under supervisor with a different php.ini, or the web config while the failure is on CLI. Run php --ini to see exactly which files the CLI loads, and use phpinfo() or a route for the web side. Also check for overrides: ini_set() calls in bootstrap files, .htaccess php_value lines, and host panels (cPanel MultiPHP INI Editor, Plesk, RunCloud) can all beat php.ini in different ways.
In Laravel, start in storage/logs/laravel.log — the same log you would check for database connection failures (my walkthrough of the SQLSTATE[HY000] [2002] "Connection refused" error lives at https://webixyhub.com/blog/fix-sqlstate-hy000-2002-connection-refused-laravel). Confirm the exact byte count and the file/line before touching any config.
Step 2: Raise the Limit in the Right Place
Once you know which runtime failed, here is how to increase the PHP memory limit in exactly that place:
-
php.ini (server-wide):
memory_limit = 512M, then restart PHP-FPM or Apache. The cleanest fix when you own the server. -
PHP-FPM pool config:
php_admin_value[memory_limit] = 512M— per-site, survivesphp.iniresets on managed hosts. -
Apache .htaccess:
php_value memory_limit 512M— works only on mod_php, not FPM. If you get a 500 after adding it, your host blocks the directive; remove the line. -
Single script:
ini_set('memory_limit', '512M');at the top of a CLI script or Artisan command — the safest, affects nothing else. -
Single command:
php -d memory_limit=512M artisan reports:generate— no permanent change at all.
; php.ini — pick a sane production value, not -1
memory_limit = 512M
# reload the right service afterwards
sudo systemctl restart php8.3-fpm
# or Apache:
sudo systemctl restart apache2
A note on -1 (unlimited): it is fine for a Composer install or a one-off migration on your own machine. In production it is a loaded gun — one bad query and PHP eats all server RAM until the OOM killer shoots the process (or worse, takes MySQL down with it). If your first instinct is -1 in production, keep reading: you need the next section more than a bigger number.
Step 3: WordPress — Use WP_MEMORY_LIMIT Correctly
WordPress has its own layer: WP_MEMORY_LIMIT in wp-config.php. This raises the limit for front-end requests only; admin pages use WP_MAX_MEMORY_LIMIT (default 256M), which needs its own define if you want higher:
<?php
// wp-config.php — above "/* That's all, stop editing! */"
define('WP_MEMORY_LIMIT', '256M');
define('WP_MAX_MEMORY_LIMIT', '512M');
The critical detail most guides skip: WP_MEMORY_LIMIT can never exceed the server's memory_limit. If your host sets PHP's memory_limit to 128M, defining 512M in wp-config.php does absolutely nothing. Check both, and on shared hosting you usually have to raise PHP's limit first via the host panel or .user.ini — or accept 256M and fix the plugin instead.
The usual WordPress offenders: backup plugins on big media libraries, page builders saving huge layouts, WooCommerce with tens of thousands of products, and bulk importers. Deactivate suspects one at a time and re-run the failing action; the Query Monitor plugin will show you exactly which plugin's query or hook burns the memory.
Step 4: Composer and Artisan Have Their Own Memory Ceiling
The 1610612736-bytes error during composer install is so common it deserves its own fix. Composer runs as a CLI process with its own memory handling:
# one-off: unlimited memory for this install only
COMPOSER_MEMORY_LIMIT=-1 composer install
# or inline per-command for artisan
php -d memory_limit=1024M artisan migrate --force
Do not set memory_limit = -1 globally in the CLI php.ini just to make Composer happy — every future cron and queue worker inherits it. The per-command flags are the professional move: they fix the install and leave production boundaries intact.
Step 5: Fix the Code That Actually Leaks
This is the section that permanently solves the "allowed memory size exhausted" problem. Three patterns cover most Laravel memory exhaustion I see on client projects.
Pattern 1: unbounded queries. Model::all() or ->get() on a big table hydrates every row into an object. Ten thousand users is a slow page; a million rows is a fatal error. Use chunk() for batch updates and cursor() for streaming exports:
<?php
// BAD: loads every user into memory at once
$users = User::all();
foreach ($users as $user) {
$user->notify(new MonthlyReport());
}
// GOOD: constant memory no matter how big the table gets
User::chunk(500, function ($users) {
foreach ($users as $user) {
$user->notify(new MonthlyReport());
}
});
<?php
// For exports and reports: cursor() streams one row at a time
$handle = fopen('php://output', 'w');
foreach (Order::cursor() as $order) {
fputcsv($handle, [$order->id, $order->total, $order->created_at]);
}
fclose($handle);
chunk() supports eager loading per batch (see my walkthrough of fixing the N+1 query problem in Laravel at https://webixyhub.com/blog/fix-n1-query-problem-laravel-eager-loading); cursor() is leaner but cannot eager-load. Pick chunk() for updates with relations, cursor() for plain row streaming.
Pattern 2: fetching more columns than you need. Select only what you use:
<?php
// Fetches every column, including longtext blobs you never read
$posts = Post::where('status', 'published')->get();
// Fetches 3 columns — often 10x less memory
$posts = Post::where('status', 'published')
->select('id', 'title', 'slug')
->get();
Pattern 3: debug and cache data that grows. Laravel Debugbar, DB::enableQueryLog() left on, and log channels that buffer stack traces all accumulate per request. In long Artisan commands, call DB::disableQueryLog(), unset() large temp variables, and avoid holding collections you already wrote out. And if you store big structures in cache or session, remember that unserializing them costs the same memory all over again on every read.
Step 6: Queue Workers and Image Processing Are the Silent Killers
Two production patterns trigger "allowed memory size exhausted" without any obvious bug.
Long-running queue workers accumulate memory across hundreds of jobs — Eloquent models, logs, and container bindings pile up. This is the same class of problem I cover when webhooks misbehave (my guide to fixing Stripe webhook signature verification failures in Laravel: https://webixyhub.com/blog/fix-stripe-webhook-signature-verification-failed-laravel). Cap workers so they restart before leaks accumulate:
# restart the worker before leaks accumulate
php artisan queue:work --memory=256 --max-jobs=500 --max-time=3600
Image processing is the other classic: GD loads the full decompressed bitmap into memory, so a 10MB JPEG can need 200MB+ of RAM. Process uploads on the queue — never in the web request — cap worker memory, and for heavy pipelines prefer the libvips driver (Intervention Image supports it), which streams instead of loading whole images.
Common Pitfalls and Their Fixes
- "I raised memory_limit but nothing changed." You edited the wrong
php.ini(CLI vs FPM), forgot to restart the service, or a.user.ini, pool config, orini_set()is overriding you. Verify with the Step 1 snippet after the change. - Limit written as
512MB. PHP reads the trailing B as bytes — your limit becomes 2MB, not 512MB. Use512M. - White screen in WordPress with no error. Error display is off and the log is elsewhere. Enable
WP_DEBUGandWP_DEBUG_LOGinwp-config.phpand readwp-content/debug.log. - Error only during imports or cron. Those run on CLI or a different worker with a different limit — check that runtime specifically, and consider chunking the import.
- Error right after a plugin or theme update. Something new is allocating. Roll back or deactivate to confirm, then profile before raising limits.
The Quick Checklist
- Reproduce and note which runtime failed (web, CLI, cron, worker).
- Read the real limit for that runtime —
php -i,php --ini, orini_get(). - Raise it in the right place, for that runtime only.
- Find the allocation pattern: unbounded
get(), missing chunk/cursor, image processing in the request, leaking worker. - Fix the pattern; keep the raised limit as headroom, not as the fix.
- Monitor peak usage (
memory_get_peak_usage(true)in logs or your APM) so the next regression shows up before users do.
If a memory problem — or anything else in your stack — keeps eating your week, I debug and harden Laravel and WordPress applications for clients worldwide. Tell me what is breaking at https://webixyhub.com and I will take a look.
Top comments (0)