A PHP out of memory error can feel unreasonable. The application may have a PHP memory limit of 256 MB or 512 MB, yet a seemingly ordinary import, report, image upload, or background job still crashes with an Allowed memory size exhausted message. Raising the limit may make the failure disappear temporarily, but it rarely addresses or fix the workload that caused it.
The underlying issue is usually not one dramatic allocation. PHP applications often create several copies of the same data as it moves through file readers, arrays, database abstractions, serializers, validation layers, and framework collections. Modern architectures also make memory behavior more visible: queue workers remain alive for many jobs, ORM models carry substantial metadata, and containers may terminate a process before PHP reaches its configured limit.
Effective PHP memory optimization begins by measuring allocation patterns and changing workflows so they process bounded amounts of data. Here are the most common causes, diagnostic techniques, and practical solutions.
What the PHP Memory Limit Actually Controls
The memory_limit directive defines how much memory a PHP script may allocate through PHP’s memory manager. You can review its behavior in the official PHP documentation. A value of -1 removes the limit, but that is dangerous in production because one process can consume all available container or server memory.
The configured number is not always the same as a process’s resident memory reported by the operating system. Native extensions, shared libraries, allocator behavior, JIT resources, and external image libraries can affect process memory differently. A container may also have a 512 MB cgroup limit while PHP is configured for 1 GB. In that case, the operating system can kill the process without producing the familiar PHP memory limit exhausted message.
A generous limit therefore provides headroom, not proof that the workflow is efficient. If one request needs hundreds of megabytes, concurrent requests can still overwhelm the host.
Common Causes of Excessive PHP Memory Usage
Loading Entire Files Into Memory
file_get_contents() is convenient, but it creates a string containing the entire file. Parsing that string can produce additional copies and data structures. A 150 MB JSON file might require the original string, a decoded PHP array, normalized records, and temporary values. Peak PHP memory usage can become several times larger than the file itself.
CSV imports have the same problem when developers read every row into an array before validation or insertion. PHP large file processing should normally be incremental, especially for user uploads, exports, logs, XML feeds, and object-storage downloads.
Fetching Large Database Result Sets
fetchAll(), ORM get() methods, and eager loading can place an entire result set in memory. PHP database result sets become particularly expensive when each row is hydrated into a model with attributes, casts, relationships, and framework state.
A query returning 100,000 small rows may use far more memory as PHP objects than the raw database payload. Selecting unnecessary columns and loading relationships inside loops compound the problem. The query may be fast while model hydration causes the PHP out of memory error.
Oversized Arrays and Duplicate Transformations
PHP arrays are ordered hash maps rather than compact, fixed-size arrays. Their flexibility carries significant overhead for buckets, keys, and zvals. Consequently, PHP arrays memory usage can be much larger than the apparent payload.
Operations such as array_map(), array_filter(), array_values(), and collection pipelines often return new arrays. Chaining them may keep multiple full-sized structures alive during one expression. Repeatedly merging arrays in a loop is even worse because each merge can allocate and copy an increasingly large result.
Retaining Objects and References
PHP object memory management is automatic, but memory cannot be reclaimed while a reachable reference remains. Static caches, service singletons, event listeners, closures, global registries, and debugging collectors can unintentionally retain complete object graphs. A closure that captures one model may also preserve related objects attached to it.
A subtle issue occurs after a reference-based loop. The loop variable remains a reference to the last element unless it is explicitly unset:
<?php
foreach ($records as &$record) {
$record['processed'] = true;
}
unset($record);Circular references can be collected by PHP’s cycle collector, but collection is not necessarily immediate. Large cyclic graphs can therefore increase peak memory before garbage collection runs.
Inefficient Loops and Accidental Accumulation
A loop that appears incremental may still append every result to an array, retain every exception, or build one enormous log string. Concatenating report output before writing it has the same effect as loading a large file: the complete result remains in memory.
Watch for per-iteration queries, repeated serialization, collection conversions, and temporary variables whose lifetime extends beyond the iteration. The key question is whether memory should remain stable as the loop progresses. If it grows with every item, the process is retaining data.
Image Processing and Long-Running Jobs
Compressed image size is a poor predictor of processing memory. A 12 MB JPEG can expand into a large uncompressed pixel buffer, and resizing may require source, destination, and intermediate buffers simultaneously. Dimensions, color channels, bit depth, and library overhead matter more than upload size.
Queue workers, scheduled consumers, WebSocket services, and persistent runtimes introduce another challenge. Traditional PHP-FPM requests release process-level state after each request, but a worker may execute thousands of jobs. Static state, framework services, third-party SDKs, and extension-level allocations can accumulate across jobs, making Laravel memory usage or another framework’s worker memory rise gradually.
Memory Leaks Versus Necessary but Excessive Allocation
Not every growing process has a genuine memory leak. If an application loads one million rows and intentionally stores them, it is allocating more memory than necessary for the task, but PHP has not lost track of that memory. The fix is a bounded workflow, not a garbage collector setting.
A genuine leak occurs when memory cannot be recovered after the related work and references are gone, often because of a runtime or extension defect. Application-level retention is more common: a cache, listener, container, or static property still references the data. Allocator fragmentation can also keep the process’s resident memory high even when PHP can reuse that space internally.
Test with a stable, repeatable unit of work. If active usage returns near its baseline while resident memory levels off, reuse or fragmentation is likely. If active usage climbs after every identical iteration, investigate retained references or a genuine leak.
How to Perform PHP Memory Profiling
Start PHP memory debugging with built-in measurements. memory_get_usage() reports current usage, while memory_get_peak_usage() reports the request’s high-water mark. Passing true includes memory allocated from the system by PHP’s memory manager, which may exceed currently used memory.
<?php
function memoryCheckpoint(string $label): void
{
printf(
'%s: used %.2f MB, allocated %.2f MB, peak %.2f MB' . PHP_EOL,
$label,
memory_get_usage() / 1048576,
memory_get_usage(true) / 1048576,
memory_get_peak_usage(true) / 1048576
);
}
memoryCheckpoint('before query');
$rows = $repository->loadReportRows();
memoryCheckpoint('after query');Place checkpoints before and after database retrieval, decoding, transformations, image creation, and serialization. Do not log every iteration in production; sample every few hundred or thousand items to avoid generating another resource problem.
For deeper PHP memory profiling, use Xdebug traces in development, Blackfire or Tideways for production-oriented profiling, and heap inspection tools such as php-meminfo. Compare PHP metrics with container and operating-system RSS measurements. Also record input size, row count, image dimensions, and job number so growth can be correlated with workload.
Practical PHP Memory Optimization Techniques
Stream Large Files Instead of Reading Them All
PHP streaming large files keeps memory approximately constant because only a line or buffer is active at one time:
<?php
$handle = fopen($path, 'rb');
try {
while (($row = fgetcsv($handle)) !== false) {
processRow($row);
}
} finally {
fclose($handle);
}For downloads and exports, write directly to an output stream rather than building the complete response body. For JSON, use a streaming parser when the document is too large to decode safely. Streaming works only when downstream code also avoids collecting every parsed item.
Use PHP Generators for Lazy Pipelines
PHP generators yield one value at a time and preserve only the generator’s current execution state. The PHP generators documentation explains their iteration model.
<?php
function csvRows(string $path): Generator
{
$handle = fopen($path, 'rb');
try {
while (($row = fgetcsv($handle)) !== false) {
yield $row;
}
} finally {
fclose($handle);
}
}
foreach (csvRows($path) as $row) {
validateAndPersist($row);
}A generator does not make an eager source lazy. Yielding rows from an array already created by fetchAll() saves little. Laziness must begin at the file handle, cursor, API pagination layer, or database query.
Process Database Records in Chunks
Retrieve a controlled batch, process it, release it, and then request the next batch. Prefer keyset pagination such as WHERE id > :last_id ORDER BY id LIMIT 500 over increasingly large offsets. It is usually faster and avoids skipped or duplicated positions when data changes.
In Laravel, use chunkById() for batch updates and lazyById() when a lazy stream fits the workflow. Select only required columns, avoid unnecessary model hydration, and use lower-level query results for data pipelines that do not need model behavior.
Release References and Bound Worker Lifetimes
Use unset() for large values once their work is complete, close file and network handles, clear framework entity managers where appropriate, and avoid unbounded static caches. gc_collect_cycles() can help after a known cycle-heavy batch, but calling it constantly may reduce performance and will not free reachable data.
Long-running workers should have operational limits. Restart them after a defined number of jobs, elapsed time, or memory threshold. This is not a substitute for fixing growth, but it provides a safety boundary for fragmentation and third-party libraries. Set worker and container limits coherently so the process exits gracefully before the platform kills it.
Redesign Memory-Heavy Workflows
The strongest PHP application optimization often comes from architecture rather than micro-tuning. Split giant imports into idempotent batches, store intermediate state externally, upload directly to object storage, generate exports asynchronously, and process images in dedicated workers with explicit dimension limits. Validate file dimensions and record counts before expensive work begins.
After optimization, keep a realistic memory limit instead of setting it to unlimited. A controlled failure protects neighboring requests and exposes regressions during testing.
PHP Performance Tuning Checklist
- Measure current, allocated, peak, and operating-system memory.
- Profile with production-like files, row counts, images, and worker durations.
- Replace full-file reads and full-result queries with streams or chunks.
- Reduce array copies, ORM hydration, selected columns, and retained objects.
- Verify that memory stabilizes during repeated batches or jobs.
- Align PHP, worker, container, and infrastructure memory limits.
Frequently Asked Questions
Why does Allowed memory size exhausted show a number above my data size?
The data’s file or transfer size excludes PHP array overhead, objects, decoded representations, indexes, temporary copies, and library buffers. Parsing and transforming data can require several times its original size.
Should I simply increase the PHP memory limit?
A modest increase can be appropriate when a measured workload genuinely needs more headroom. It should not be the first response to unbounded queries, full-file loading, or growing workers. Raising the limit can shift the failure to the server or container level.
Do generators always reduce PHP memory usage?
Only when the complete pipeline is lazy. A generator that iterates over an already populated array still pays for that array. It is most effective when connected directly to a stream, paginated source, or unbuffered data producer.
Why does a queue worker use more memory after every job?
The worker may retain static caches, listeners, service objects, model graphs, logs, or extension allocations. Profile memory between identical jobs, clear job-scoped state, inspect captured references, and configure controlled worker recycling while investigating the source.