Reducing HTTP Latency with Symfony Messenger Asynchronous Queues
Stop making users wait for heavy tasks. Learn how to use Symfony Messenger to decouple long‑running processes from HTTP requests for faster response times.
17 Sept 2026, 02:20 UTC

The Cost of Synchronous Processing
When a user uploads a profile picture or triggers a report generation, the intuitive approach is to handle that logic inside the controller. However, if your application must resize an image, call a third‑party API, or generate a PDF, the user is forced to wait for these operations to complete before receiving an HTTP response. This creates a bottleneck: your web server threads stay occupied longer, latency spikes, and the user experience suffers.
The Solution: Decouple Request and Processing
The solution is to decouple the request from the processing. By using Symfony Messenger, you can dispatch a message (a simple PHP object) to a queue and immediately return a success response to the user. A separate worker process then handles the heavy lifting in the background.
Core Components of the Messenger Workflow
To move a task to the background, Symfony Messenger relies on three primary components:
- The Message: A plain PHP class (DTO) that contains only the data needed for the task (e.g., an ID), not the logic itself.
- The Message Handler: A service that contains the actual business logic to execute when a specific message is received.
- The Transport: The storage mechanism (the "queue") where messages sit until a worker picks them up. Common transports include Redis, RabbitMQ, or a database table via Doctrine.
Worked Example: Asynchronous Image Processing
Assume we are using Symfony 6.x or 7.x. We want to generate a thumbnail after a user uploads an image without making them wait for the image manipulation library to finish.
1. Define the Message
// src/Message/ProcessImageMessage.php
namespace App\Message;
class ProcessImageMessage
{
public function __construct(
private int $imageId
) {}
public function getImageId(): int
{
return $this->imageId;
}
}
2. Create the Handler
// src/MessageHandler/ProcessImageHandler.php
namespace App\MessageHandler;
use App\Message\ProcessImageMessage;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
class ProcessImageHandler
{
public function __invoke(ProcessImageMessage $message)
{
// Simulate heavy image processing
$imageId = $message->getImageId();
// Logic to resize image and save to storage goes here
sleep(5);
}
}
3. Configure the Transport
In config/packages/messenger.yaml, define a transport and route the message to it. Using Doctrine is the simplest way to start as it requires no extra infrastructure beyond your database.
framework:
messenger:
transports:
async: 'doctrine://default'
routing:
'App\Message\ProcessImageMessage': async
4. Dispatch from the Controller
Inject the MessageBusInterface to send the message to the queue.
// src/Controller/ImageController.php
namespace App\Controller;
use App\Message\ProcessImageMessage;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route;
class ImageController extends AbstractController
{
#[Route('/upload', name: 'app_upload')]
public function upload(MessageBusInterface $bus)
{
$imageId = 123; // Assume image was saved to DB
$bus->dispatch(new ProcessImageMessage($imageId));
return $this->json(['status' => 'Image uploaded; processing started.']);
}
}
Running and Verifying the Worker
Dispatching the message only places it in the database. To actually process the image, you must run the consumer command from your terminal (as a user with permissions to execute PHP binaries):
php bin/console messenger:consume async -vv
Verification Steps:
- Trigger the
/uploadroute. The response should be nearly instantaneous (usually <200ms). - Check your database table
messenger_messages. You will see a row appear immediately after the request. - Run the
messenger:consumecommand. The row in the database should disappear as the worker processes the task.
Trade‑offs and Infrastructure Risks
Moving to an asynchronous architecture introduces new failure modes that do not exist in synchronous code:
- Visibility: The user no longer knows immediately if the task failed. You must implement a way to notify the user (e.g., WebSockets or polling) or log failures.
- Zombie Workers: Workers are long‑running PHP processes. They can leak memory or crash. In production, you must use a process manager like Supervisor to ensure workers are automatically restarted.
- Message Duplication: Depending on the transport and acknowledgment settings, a message might be processed twice if the worker crashes after finishing the task but before acknowledging it. Ensure your handlers are idempotent (running them twice has the same effect as running them once).
Closing Action
To implement this, start by identifying the slowest 10% of your request‑response cycles. If a task does not need to return data to the user's screen immediately, move it to a doctrine:// transport for testing, then migrate to redis:// for higher throughput as your volume grows.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.