Memory Leak
debt(d7/e5/b5/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints list xdebug, blackfire, and phpstan — these are specialist tools, but memory leaks in long-running workers typically manifest only under sustained load or after many iterations, meaning they are often not caught until runtime testing or production monitoring. phpstan cannot catch runtime accumulation patterns; xdebug/blackfire require deliberate profiling sessions. The leak is silent in normal short-lived FPM requests and only surfaces in CLI/queue contexts under load, pushing this above d5.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix lists several mitigations (unset() variables, avoid circular refs, use generators, monitor RSS), but these are not single-line fixes. Common mistakes include static caches with no eviction, unremoved event listeners, and circular references — each requires identifying the offending pattern across potentially many places in a worker's lifecycle. It is not a single-call swap (e1/e3) nor a full architectural rework (e7+), but a meaningful refactor within the affected worker component(s).
Closest to 'persistent productivity tax' (b5). The applies_to scope is limited to cli and queue-worker contexts (not all PHP), which reduces reach compared to a universal concern. However, within those contexts, the risk shapes every design decision: caching strategies, event-listener patterns, and object lifecycle management must all be considered. Any developer working on long-running PHP processes carries this mental overhead continuously, making it a persistent tax on those work streams.
Closest to 'serious trap' (t7). The misconception field states exactly the trap: developers believe PHP's garbage collector prevents all memory leaks. This contradicts how GC is understood in other languages and in PHP's own short-lived request model — the GC does handle circular refs in short scripts, so the assumption feels justified. The leap to long-running workers where static properties, event listeners, and growing caches accumulate is non-obvious and contradicts the developer's prior correct experience with PHP, placing this at t7.
Also Known As
TL;DR
Explanation
PHP processes under PHP-FPM are recycled after a configurable number of requests (pm.max_requests), which masks memory leaks in normal web serving. However, in long-running processes (CLI scripts, job workers, Fibers, ReactPHP) leaks accumulate indefinitely. Common PHP causes include: circular references not collected by the cyclic garbage collector (fixed in PHP 5.3+), accumulating event listeners, large global arrays, and caching without eviction. Use memory_get_usage() or Xdebug's memory profiler to diagnose, and unset() large variables and circular structures when done.
Common Misconception
Why It Matters
Common Mistakes
- Circular references between objects — PHP's garbage collector handles most but not all cyclic structures.
- Growing static arrays or caches with no eviction — each request adds entries that never get freed.
- Event listeners registered but never removed — the listener holds a reference keeping the listened-to object alive.
- Not using weak references for observer patterns — strong references in event systems are a common leak source.
Code Examples
// Circular references prevent garbage collection before PHP 5.3 cycle collector
class Node {
public ?Node $parent = null;
public array $children = [];
}
$parent = new Node();
$child = new Node();
$child->parent = $parent; // child → parent
$parent->children[] = $child; // parent → child (cycle)
unset($parent, $child); // cycle collector must clean up
// Use WeakReference to break cycles
class Node {
public ?\WeakReference $parent = null;
public array $children = [];
}
$parent = new Node();
$child = new Node();
$child->parent = \WeakReference::create($parent); // won't prevent GC
$parent->children[] = $child;
// Retrieve: $child->parent?->get()
// In long-running scripts (queues, CLI): track memory
if (memory_get_usage(true) > 200 * 1024 * 1024) {
gc_collect_cycles();
}