dmitry_kv7 Mar 2025 17:42

Running a PHP worker process that processes a queue. After 6 hours, RSS is 800MB, started at 90MB. The leak is somewhere but profiling with Xdebug memory tracking is impractical at this scale.

What strategies do you use to find and fix memory leaks in daemon-style PHP processes?

Replies (7)
alex_petrov7 Mar 2025 17:48

Blackfire works on CLI processes too. Run the worker, profile a slice of it, look for growing reference cycles in the timeline. Reference cycles that PHP GC does not collect on time are the most common cause.

0
nphp7 Mar 2025 18:15

The most common sources I have seen: event listeners that accumulate closures, ORM identity maps that never get cleared, monolog handlers buffering in memory, static arrays used as caches with no eviction.

0
dmitry_kv7 Mar 2025 19:09

Explicitly call gc_collect_cycles() every N iterations during development and watch if memory drops. If it does, you have a reference cycle. If it does not, you have an actual growing data structure.

PHP
<?php
class Node {
public ?Node $next = null;
public string $data;
public function __construct(string $d) { $this->data = $d; }
}
// create a reference cycle
$a = new Node('a');
$b = new Node('b');
$a->next = $b;
$b->next = $a; // cycle!
$before = memory_get_usage();
unset($a, $b);
$afterUnset = memory_get_usage();
$collected = gc_collect_cycles();
$afterGc = memory_get_usage();
echo "After unset: " . ($afterUnset - $before) . " bytes delta\n";
echo "Cycles collected: {$collected}\n";
echo "After gc: " . ($afterGc - $before) . " bytes delta\n";
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
0
vova7 Mar 2025 19:35

In Swoole contexts: anything stored in a static property persists for the lifetime of the process across all coroutines. That is the first place to look. Logger instances, config arrays, caches.

0
petr_sys7 Mar 2025 20:40

We added memory_get_usage() logging every 100 jobs with a warning threshold. Found that one job type was loading full Eloquent collections without chunking. Classic.

0
sergey_web7 Mar 2025 22:11

WeakReference was added in PHP 8.0 and helps with cache-style patterns. If you hold a cache of objects that nothing else references, wrap them in WeakReference so GC can collect them.

0
katedev7 Mar 2025 23:26

If you cannot fix the leak quickly, pm.max_requests on FPM workers or a restart threshold in your worker loop (restart after N jobs) is a pragmatic mitigation.

0