Memory leak in Swoole workers — static variables survive between requests
Ran into a nasty issue last week. Our Hyperf app started eating RAM after about 2–3 hours of traffic, workers had to be restarted manually.
After profiling with swoole_tracemalloc and reading through the code carefully, we found the root cause: static class properties that we were using as in-memory caches were never reset between requests because Swoole reuses the same PHP process for thousands of requests — unlike PHP-FPM where each request is a clean process.
Example of the problem:
This cache grows indefinitely. In FPM it resets on every request. In Swoole it lives for the lifetime of the worker.
We ended up switching to coroutine-local storage using Swoole\Coroutine::getContext() for request-scoped caches, and moved longer-lived caches to Redis with TTLs.
But I am still not 100% sure this is the only place. Are there other common patterns that cause leaks in long-running Swoole apps? Curious what others have encountered.
Yeah, had exactly the same thing. Static variables are the most common one, but there are others:
- Event listeners registered globally inside a request handler — if you register them in a service that gets instantiated per-request, listeners pile up on the same dispatcher instance
- Unclosed generators — not GC’d immediately in coroutine context
- Guzzle with persistent connections improperly configured — connection pool grows unbounded
For the static cache case specifically, Hyperf’s Coroutine::getContext() approach is correct. Something like:
Context is automatically destroyed when the coroutine ends, so no leaks.
One more thing worth checking — Hyperf\Utils\Context is the Hyperf-idiomatic wrapper around exactly that coroutine context, you don’t need to use the Swoole API directly:
Also, check your DB connection pool config. If max_connections is set too high and connections are not released properly (exception thrown before finally), that’s another slow leak that shows up under load. We set up a cron that logs pool stats every 5 minutes to catch this early.
```php blocks are runnable.