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

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

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 application
  • api-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-queue container, 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:

platform.app.yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

  workers:
    queue:
      commands:
        start: |
          while true; do
            echo "Worker is restarting..."
            php bin/console messenger:consume async --memory-limit=128M --time-limit=120 -q
            echo "Worker exited, restarting in 1 second..."
            sleep 1
          done          

Why this works:

  • --memory-limit=128M forces the worker to restart if memory exceeds 128MB
  • --time-limit=120 ensures the worker exits after 2 minutes, even if idle
  • The while true loop 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

Ready to Elevate Your Business with a Professional Website?

Join businesses that trust us for expert website development, hosting, and management to ensure high performance and reliability.