Solving Symfony Messenger Memory Leaks on Platform.sh with Worker Restart Strategy

Table of Contents
Introduction
A client running their PHP application on Platform.sh faced a growing performance issue related to Symfony Messenger. The memory usage of the background worker (Messenger consumer) steadily increased over time—even when there were no jobs to process. Under load, such as processing a CSV file with 5000 rows, memory usage would spike drastically (up to 450% more) and remain high.
This performance degradation made the system unstable over time and threatened production reliability. We stepped in to investigate and implement a robust, resource-efficient solution.
Background Architecture
The application was structured using the official Platform.sh Symfony project template, which separates the main API (api) from the background job processor (api-queue):
api: The main Symfony PHP applicationapi-queue: The Symfony Messenger worker service, running in a separate container- Doctrine ORM was used as the default transport for Messenger
This architecture allowed for clean job delegation, but also introduced complexity in managing long-running workers.
The Problem
- Memory usage kept climbing in the
api-queuecontainer, even when idle - Processing CSV jobs caused RAM usage to spike by over 450%
- Symfony Messenger combined with Doctrine transport was likely causing memory leaks
- Once memory use peaked, it stayed high, degrading performance until the container was manually restarted
Our Solution
🔁 Periodically Restarting the Worker
We adopted a controlled worker restart strategy to cap memory usage and avoid persistent bloat. This was implemented directly in the service configuration YAML:
| |
Why this works:
--memory-limit=128Mforces the worker to restart if memory exceeds 128MB--time-limit=120ensures the worker exits after 2 minutes, even if idle- The
while trueloop ensures the worker automatically restarts, preserving long-term availability
This approach creates a self-healing worker that caps memory usage and resets stale connections.
Improving Doctrine Transport Configuration
To reduce unnecessary retries and prevent runaway CPU/memory usage:
- We added explicit logic to fail jobs if the uploaded CSV file was incomplete
- This prevented the Messenger worker from retrying doomed jobs, which previously led to resource exhaustion
By recognizing unresolvable failures early, the system became much more stable and predictable under load.
The Outcome
After implementing the restart loop and refining job failure logic:
- Memory usage became consistent and predictable, even under job load
- Idle containers no longer leaked memory over time
- System-wide performance improved, and no manual restarts were needed
- The client gained deeper confidence in Symfony Messenger and Platform.sh container behavior
This lightweight solution helped the team avoid unnecessary scaling or debugging debt, and restored a healthy dev/prod feedback loop.
Technologies Used
- Platform.sh for hosting and isolated service containers
- Symfony (PHP) for application logic and job workers
- Messenger (Symfony) for asynchronous task processing
- Doctrine ORM as the Messenger transport
- YAML configuration for worker setup and service definition