
September 08, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
PHP memory limits and common leak patterns cause more production outages than most teams expect. A CSV export, a report job, or a single eager-loaded query can push a request past memory_limit and return HTTP 500 with no useful stack trace. On real client projects — from Laravel eCommerce platforms to legal-tech portals — I've traced the same failures back to misconfigured limits, unbounded collections, and code that looks fine on small datasets. This guide covers how PHP allocates memory, where leaks actually hide, and what to change in production without guessing.
memory_limit per request or CLI process. Leaks usually come from unbounded arrays, circular references before PHP 7.4 GC, static caches, and loading entire datasets into RAM — not true kernel-level leaks.What is PHP memory_limit and how does allocation work?
Every PHP process gets a ceiling set by memory_limit. When usage crosses that ceiling, PHP throws a fatal error: Allowed memory size exhausted. The limit applies per script execution — each HTTP request through PHP-FPM gets its own budget. CLI commands like artisan queue:work get a separate budget per worker process.
PHP tracks allocated bytes internally. You can inspect usage at any point with memory_get_usage(true) for system allocation and memory_get_peak_usage(true) for the high-water mark. The official PHP memory functions documentation describes both flags.
How PHP interprets memory_limit values
memory_limit accepts suffixes: 128M, 512M, 1G. A value of -1 means unlimited — avoid that in production. Setting 0 also means unlimited on most builds. The default in php.ini is often 128M, which is too low for modern Laravel 13 or WordPress 7.1 admin screens with several plugins active.
; php.ini or pool override
memory_limit = 256M
; Per-directory .user.ini (CGI/FPM with user_ini.filename enabled)
memory_limit = 512M PHP 8.5 improves allocator behaviour compared with PHP 7.x, but the mental model stays the same. Strings, arrays, and objects live on the heap. Copy-on-write reduces duplication until you modify data. Large string concatenation in loops still allocates fresh buffers each iteration — a pattern I still see in legacy export scripts.
How do you configure PHP memory limits in production?
Configuration layers stack. Know which layer wins before you change anything. On Ubuntu servers I maintain for Linux system administration clients, the wrong layer is the usual reason a php.ini edit appears to do nothing.
Configuration precedence from lowest to highest
- Built-in PHP default (often 128M)
- Global
/etc/php/8.5/fpm/php.inior Apache equivalent - Pool-specific overrides in
/etc/php/8.5/fpm/pool.d/www.conf php_admin_value[memory_limit]in the pool — cannot be overridden byini_set()ini_set('memory_limit', '512M')at runtime — if not blocked by admin values
; /etc/php/8.5/fpm/pool.d/www.conf
php_admin_value[memory_limit] = 256M
; Long-running queue workers — separate pool
[queue]
user = www-data
group = www-data
listen = /run/php/php8.5-fpm-queue.sock
pm = static
pm.max_children = 4
php_admin_value[memory_limit] = 512M
php_admin_value[max_execution_time] = 0 After any change, reload PHP-FPM: sudo systemctl reload php8.5-fpm. Verify with a one-liner:
php -i | grep memory_limit
php-fpm8.5 -i 2>/dev/null | grep memory_limit Choosing sensible limits by workload type
| Workload | Suggested limit | Notes |
|---|---|---|
| Public web pages (Laravel, WordPress) | 128M–256M | Fix the code if you need more for normal pages |
| Admin dashboards, bulk edits | 256M–512M | Isolate to admin routes or separate pool |
| CSV/Excel exports | 512M–1G | Prefer streaming or queued jobs instead |
| Queue workers (Maatwebsite Excel, PDF) | 512M–1G | Match pm.max_children to server RAM |
| One-off CLI migrations | 1G–2G | Temporary; revert after migration completes |
A server with 8 GB RAM and 20 PHP-FPM workers at 512M each can theoretically demand 10 GB. That mismatch causes OOM kills at the OS level — worse than a clean PHP fatal. Capacity planning belongs in testing and optimization, not only in application code.
What are the most common PHP memory leak patterns?
Strictly speaking, PHP rarely leaks memory the way C programs do. When usage climbs across a single request, the cause is usually retained references — data you loaded and never released. Between requests, PHP-FPM workers reset. Long-running queue workers are the exception; they need deliberate cleanup.
Pattern 1: Loading entire tables into memory
This is the number-one offender on Laravel apps. User::all() or Order::with('items')->get() on a table with 200,000 rows will exhaust 512M quickly. Eloquent hydrates full model objects — far heavier than raw arrays.
/* Bad — loads everything */
$orders = Order::with(['items', 'customer'])->get();
/* Better — stream in chunks */
Order::with(['items', 'customer'])
->orderBy('id')
->chunkById(500, function ($orders) {
foreach ($orders as $order) {
/* process one batch */
}
});
/* Best for exports — lazy cursor */
foreach (Order::cursor() as $order) {
/* one model at a time */
} On a production Laravel application handling trek bookings, switching a report from get() to chunkById() dropped peak memory from 780M to under 90M. Same output, different architecture. See handling large JSON payloads in PHP for related patterns with API responses.
Pattern 2: Unbounded in-memory aggregation
Building a full index inside a loop is another classic mistake. Reading a 500 MB log into one string, then splitting on newlines, doubles peak usage before GC runs.
/* Bad */
$lines = file('/var/log/app.log');
$counts = [];
foreach ($lines as $line) {
$counts[$line] = ($counts[$line] ?? 0) + 1;
}
/* Better — stream line by line */
$handle = fopen('/var/log/app.log', 'r');
$counts = [];
while (($line = fgets($handle)) !== false) {
$counts[$line] = ($counts[$line] ?? 0) + 1;
}
fclose($handle); Pattern 3: Static properties and singleton caches
Static arrays on service classes survive for the entire worker lifetime. In queue workers processing thousands of jobs, a static cache grows without bound.
class GeoLookup
{
private static array $cache = [];
public static function resolve(string $code): array
{
if (!isset(self::$cache[$code])) {
self::$cache[$code] = /* expensive lookup */;
}
return self::$cache[$code];
}
} Use Redis or application-level Redis caching with TTL instead. Static caches belong only in short web requests where the worker resets after each hit.
Pattern 4: Circular references and closures
PHP 7.4+ handles most circular reference graphs through cycle collection. Older code holding parent-child object graphs in long CLI scripts can still balloon. Closures that capture large objects by reference keep those objects alive until the closure is destroyed.
$parent = new stdClass();
$child = new stdClass();
$parent->child = $child;
$child->parent = $parent;
unset($parent, $child);
/* GC runs at cycle boundaries — force if needed */
gc_collect_cycles(); Pattern 5: Image and document processing
GD and Imagick decode full raster images into memory. A 6000×4000 PNG can consume hundreds of megabytes per intermediate step. Spatie Media Library conversions on large uploads are a common trigger. Process on a queue worker with a higher limit, or delegate to a dedicated service.
Pattern 6: Serialisation and deep copy side effects
Serialising large object graphs for cache or queue payloads duplicates structure in memory during encode/decode. Passing massive arrays through json_encode for logging can spike usage on every request. Use the JSON formatter tool to inspect payload size during development, and truncate debug output in production.
How do you debug PHP memory exhaustion in production?
Reproducing memory failures locally with production data volume is the fastest path to a fix. When that is not possible, instrument the code and read server metrics.
Step-by-step debugging workflow
- Confirm the active limit: add a temporary route or CLI script calling
ini_get('memory_limit'). - Find peak usage in the failing path with
memory_get_peak_usage(true)at strategic checkpoints. - Enable query logging temporarily and count rows returned — N+1 queries multiply memory as well as SQL round trips.
- Run the script under
php -d memory_limit=512M artisan …with Xdebug disabled for accurate timing. - Profile with PHPStan and Blackfire or Xdebug trace if the hotspot is unclear.
- Load-test the fixed path with k6 load testing before deploy.
function logMemory(string $label): void
{
$current = memory_get_usage(true);
$peak = memory_get_peak_usage(true);
logger()->debug($label, [
'current_mb' => round($current / 1048576, 2),
'peak_mb' => round($peak / 1048576, 2),
]);
}
logMemory('after bootstrap');
/* ... code under test ... */
logMemory('after export loop'); Reading error logs and PHP-FPM signals
PHP fatals land in the FPM pool log or nginx/apache error log depending on setup. Look for Allowed memory size of X bytes exhausted followed by the file and line. If the line is inside vendor code, your caller probably passed too much data — trace upward.
Watch for Linux OOM killer entries in dmesg. Those mean total process memory exceeded physical RAM plus swap. Lower pm.max_children or per-worker limits. PHP-FPM tuning for high traffic covers the arithmetic.
Laravel-specific debugging tools
Laravel Telescope and Debugbar show query counts in development. Never enable Debugbar in production — it retains request data in memory and skews measurements. For queued jobs, wrap the handle method:
public function handle(): void
{
ini_set('memory_limit', '512M');
$before = memory_get_usage(true);
/* job logic */
$after = memory_get_usage(true);
Log::info('Job memory', [
'delta_mb' => round(($after - $before) / 1048576, 2),
'peak_mb' => round(memory_get_peak_usage(true) / 1048576, 2),
]);
} The Laravel documentation on chunking query results is the reference for ORM-level fixes in Laravel 12 and 13.
How do WordPress, WooCommerce, and CLI workers differ?
WordPress 7.1 sets WP_MEMORY_LIMIT and WP_MAX_MEMORY_LIMIT in wp-config.php. WooCommerce 11.1 admin screens and product imports often need 256M or more. That does not excuse loading every product meta row into one array — the same chunking rules apply.
define('WP_MEMORY_LIMIT', '256M');
define('WP_MAX_MEMORY_LIMIT', '512M'); CLI workers on Laravel 13 with PHP 8.3+ run until killed. Memory from job 1 can affect job 500 if static state leaks. Restart workers periodically with --max-jobs=500 or supervisor stopwaitsecs. On shared hosting common in Nepal (Rs 3,000–8,000/year, ~USD 22–60), hard caps at 128M or 256M are non-negotiable — design exports as emailed links, not synchronous downloads.
For enterprise batch systems, enterprise application development should treat memory budgets as architecture requirements from day one. I've applied the same discipline on booking systems with large itinerary datasets where report generation runs off-queue.
OPcache configuration reduces bootstrap memory but does not shrink per-request data. Pair opcache tuning with query discipline. When a site keeps hitting limits after code fixes, support and maintenance usually reveals a plugin, module, or cron job added without load testing.
Key Takeaways
- Set
memory_limitat the FPM pool level withphp_admin_value— verify withphp -i, not assumptions. - Never load full tables with
get()orall(); usechunkById(),cursor(), or SQL aggregation instead. - Static caches and unbounded in-memory arrays are the top leak patterns in long-running queue workers.
- Raise limits only for isolated pools (exports, image jobs) — global 1G masks bugs and risks OS OOM kills.
- Instrument with
memory_get_peak_usage(true)at checkpoints before blindly doublingmemory_limit. - Match total worker memory (children × limit) to server RAM; PHP-FPM and MySQL 9.7 compete for the same box.
People Also Ask
What happens when PHP runs out of memory?
PHP terminates the script with a fatal error: Allowed memory size of N bytes exhausted. The web server returns HTTP 500 unless you catch it at a higher layer — which you generally cannot for fatals. PHP-FPM logs the error; the worker process may respawn clean for the next request.
Can ini_set increase memory_limit in Laravel?
Only if the pool does not set php_admin_value[memory_limit]. Admin values block runtime overrides. Many production pools use admin values intentionally so application code cannot raise limits without deployment review. Check ini_get('memory_limit') after bootstrap to see the effective value.
Is raising memory_limit to 2G a valid fix?
It is a temporary workaround, not a fix. If a web page needs 2G, the architecture is wrong — paginate, queue, or stream. Reserve high limits for CLI migrations or dedicated export workers with monitored concurrency. On a 4 GB VPS, one 2G request plus MySQL and Redis leaves little headroom.
Do PHP memory leaks persist between requests?
Not in standard PHP-FPM web workers — each request starts fresh after the worker handles the script. Leaks persist in long-running CLI processes: queue workers, ReactPHP/RoadRunner workers, and custom daemons. Restart policies and avoiding static accumulation are essential there.
Fix the pattern first, then tune the limit
PHP memory limits and common leak patterns are predictable once you know where to look. Misconfigured pools, unbounded Eloquent queries, and static caches cause most of the fatals I see in production — not mysterious engine bugs. Fix the data-loading pattern, isolate heavy jobs into separate FPM pools, and size worker counts against real RAM. If your application keeps hitting ceilings after code review, speed and optimization work or a focused audit from someone who ships PHP in production will save more than repeatedly editing php.ini. Need hands-on help with a stuck export, queue worker, or hosting migration? Contact us — or browse web development services and client reviews to see how similar issues were resolved on live projects.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

