FrankenPHP Worker Mode With Symfony: Memory Leaks, Service Resets and When to Turn It Off

Sandor Farkas
Founder & Lead Developer
Expert in software development and legacy code optimization
LinkedInA FrankenPHP worker mode Symfony setup promises one big thing: skip the cost of booting a fresh kernel on every request and keep your application in memory between them. That is also exactly where the trouble starts. Classic PHP has spent two decades being forgiving about sloppy state management, because every request got a clean process and a fresh memory space the instant it finished. Worker mode removes that safety net. The kernel boots once, then handles thousands of requests before it restarts, and anything that leaks or holds onto state from one request can quietly bleed into the next one.
This is a direct follow-up to our earlier piece on running FrankenPHP in production, which covers the migration path from Nginx and PHP-FPM. This post goes one layer deeper: what actually breaks when a Symfony application runs in worker mode, how to find the leaks before they show up as an out-of-memory restart in production, and the cases where turning worker mode off is the more honest answer for the codebase you actually have.
What FrankenPHP worker mode changes in a Symfony request
In classic PHP, a request comes in, PHP-FPM spins up (or reuses) a worker process, Symfony boots its kernel, builds the service container, handles the request, and the process either dies or gets recycled with a clean slate. Any object you create during that request, any static property you set, any value you push into a cache array, disappears when the request ends.
Worker mode changes the contract. FrankenPHP boots the Symfony kernel once per worker process and then runs a loop: handle a request, reset what needs resetting, handle the next one, for as long as that worker stays alive. The performance win is real, you are no longer paying the autoloading and container-compilation cost on every single request. But every service in your container now has to answer a question it was never asked before: what happens to my internal state when the next request arrives, and I am still the same PHP object I was a second ago?
Most services answer that question correctly without any extra work, because they are stateless by design: they take input, do something, return output, and hold nothing between calls. The services that answer it badly are the ones worth auditing before you flip worker mode on in production.
Where state actually leaks
Three categories cause the overwhelming majority of worker-mode incidents in Symfony applications.
Services that implement Symfony's ResetInterface get a reset() call between requests automatically, and the framework ships this for a reason: a lot of core services (the request stack, the security token storage, parts of the HTTP client) already need it. If you have custom services with internal arrays, counters, or collected data that should not survive past the request, and you have not implemented ResetInterface on them, that data is still sitting there when request two starts. A collector service that accumulates log entries for a batch job and never clears them is the textbook case: in classic mode it would die with the process and nobody would notice; in worker mode it grows until the worker restarts or runs out of memory.
Doctrine's EntityManager is the second category, and it is the one that causes the strangest bugs. After an exception, Doctrine can put the EntityManager into a closed state, and in classic PHP that closed EntityManager died with the process, so the next request got a fresh one automatically. In worker mode, a closed EntityManager can still be sitting in the container on the next request unless something resets it, and the symptom is not a clean crash, it is a confusing "entity manager is closed" error on a request that otherwise has nothing to do with whatever failed before it.
Static properties and singletons are the third category, and they are the hardest to find because they often come from a dependency rather than your own code. A static cache array that memoizes a computed value for performance inside a loop, a singleton pattern that got written before the service container existed, a class-level counter used for pagination or batching: all of these survive between requests in worker mode because the class itself stays loaded in the worker's memory, not just the object instance.
Finding leaks before production finds them for you
The honest way to detect these issues is to run the application under worker mode in a staging environment with realistic traffic and watch memory over time, not just once at startup. A worker process that climbs steadily upward request after request and never plateaus has a leak somewhere, even if nothing throws an exception. Our post on Symfony performance monitoring covers the specific metrics worth graphing, and memory per worker over request count is the one that catches worker-mode leaks specifically, well before anything user-facing breaks.
A second, cheaper check: grep your codebase for static properties on services, and for any class implementing a singleton pattern by hand rather than through the container. Neither of those is automatically a problem, but each one is a candidate you need to look at individually and decide whether the state it holds is meant to survive a request or not.
Third, run your existing test suite against a persistent worker process instead of booting a fresh kernel per test. A surprising number of state leaks that never show up in normal unit tests (because each test gets a fresh kernel) show up immediately once the same kernel instance runs the full suite in sequence.
Max requests and restarts as a safety net, not a fix
FrankenPHP supports a max_requests setting that restarts a worker after it has handled a configured number of requests, and it is tempting to treat this as a workaround for memory leaks. It is a reasonable safety net, restarting workers periodically bounds the damage from a slow leak you have not found yet, the same way restarting a classic PHP-FPM pool periodically has always been cheap insurance. But it is not a substitute for finding the leak. A worker that needs restarting every 200 requests to stay under its memory limit is going to restart constantly under real traffic, and every restart pays the full kernel boot cost that worker mode exists to avoid. If you are tuning max_requests down to compensate for a leak rather than up as routine hygiene, that is a sign to go back and find the actual source.
When classic mode is the right call
Not every Symfony application is ready for worker mode, and that is fine. If you are running a legacy codebase with services written before dependency injection discipline was consistent, especially anything containing hand-rolled singletons, global state, or static caches accumulated over years of different developers, auditing all of it before a production cutover can cost more than the performance gain is worth right now. Classic mode under FrankenPHP still gets you the single-binary deployment and the HTTP/3 support, you just lose the in-memory kernel reuse, and for a lot of applications that is an acceptable trade until the state-leak audit is done properly.
A simple readiness check before enabling worker mode: can you point to every service in your container that holds internal state across calls, and can you say with confidence what happens to that state on the next request? If the answer involves checking rather than knowing, it is worth running a focused code review first. If an application was containerized recently, our guide on containerizing a Symfony application also covers the production configuration decisions that tend to surface at the same time as worker-mode questions, since both usually come up during the same deployment overhaul.
Turning worker mode off, if you have already enabled it and are seeing unexplained errors or climbing memory, is a one-line configuration change and a safe default to fall back to while you investigate. There is no penalty for running FrankenPHP in classic mode indefinitely. The performance ceiling is lower, but a correct application in classic mode beats a fast application that occasionally serves one customer's data to another because a static cache never got reset.
If your team is weighing worker mode for a Symfony application with a few years of history behind it, a short external code review can usually tell you in a day or two whether the state-leak risk is small and specific or scattered across the codebase. Our code quality consulting work covers exactly this kind of audit, and for applications carrying older patterns throughout, legacy code optimization is the better starting point. Reach us at hello@wolf-tech.io or through wolf-tech.io if you want a second opinion before you flip the switch in production.
