Special Anniversary Black Friday: Get 30% off all training and 10% off all services Get a Quote


How To Prioritize Messages When Building Asynchronous Applications With Symfony Messenger

· Thibaut Chieux · 7 minutes to read
Blue sign on a building with several Now What? letters

Asynchronous processing offers benefits like decoupled processes and faster response times, but managing message priorities can become a challenge. When dealing with tasks ranging from password resets to complex exports, ensuring timely delivery of critical messages is essential. This article explores common asynchronous processing issues and provides solutions using Symfony Messenger, allowing you to optimize your application without extensive refactoring.

Going asynchronous sounds like a dream: decoupled processes, faster response times, and no more users staring at spinning wheels. But pretty quickly, reality hits — some messages take forever, others are way too important to be delayed, and suddenly you’re drowning in a swamp of priorities.

Whether you're firing off password reset emails or triggering complex exports, you need to make sure the right messages get through at the right time. This article dives into the problems you’ll face — and how to solve them using Symfony Messenger, without rewriting your app from scratch or crying into your logs at 3 AM.

The problem: prioritize dynamically every message

Animated gif with a lady with red glasses talking with the text "We have a priorities problem" belowWhen you start queueing messages in your Symfony app, one thing becomes obvious real fast: not all messages are equal. Some are critical and time-sensitive. Others… not so much.

Some transports already offer a way to handle priorities like:

🐰 RabbitMQ has x-priority

🌱 Beanstalkd has built-in tube priority

Nice — but what if I want to switch to another transport tomorrow without rewriting half my code?

Symfony Messenger has a way

The official documentation shows how to split messages into multiple transports based on priority. Think of it like assigning lanes on a highway: one for ambulances, one for scooters.

framework:
    messenger:
        transports:
            async_priority_high:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                options:
                    queue_name: high
            async_priority_medium:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                options:
                    queue_name: medium
            async_priority_low:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                options:
                    queue_name: low
            async_priority_very_low:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                options:
                    queue_name: very_low

        routing:
            'App\Message\ExportMessage': async_priority_low
            'App\Message\UpdateStateMessage': async_priority_high

💡 Note: The queue names are up to you — just make sure they reflect your actual use case. These are fictional examples.

Now that we’ve split messages by priority, consuming them in the right order is just as important. Thankfully, Symfony Messenger makes this super easy:

php bin/console messenger:consume async_priority_high async_priority_medium async_priority_low async_priority_very_low

The worker will first consume from async_priority_high. If it’s empty, it will try the next one. And so on. So even if there’s a backlog of non-urgent messages, high-priority ones don't get stuck waiting behind them.

How do I choose the right queue?

This was honestly the trickiest part for me. There’s no one-size-fits-all formula, and this table is crucial — everything else in this article depends on it.

👉 It obviously needs to be filled with the client or the product owner and not just dev gut feelings.

The question you’re answering here is simple, but powerful:

“What’s the maximum acceptable delay (including queue time and actual handling) for each kind of message?”

And from there, you get your priority mapping:

Priority

High

Medium

Low

Very low

Time before handling

1 minute

10 minutes

1 hour

1 day

Example

- State update

- Email

CMS update

- Prices update

- Analytics processing

- Export

- Anonymization

Now that I’ve split the messages into queues, everything should be perfect, right?

Well… not quite.

🙃 One problem remains: message types can be too generic

Let’s say I have an EmailMessage. Sounds fine. But I might use it for:

  • A password reset 🟥 High priority

  • A delivery notification 🟨 Medium

  • A “rate your purchase” ping 🟦 Low or very low

So... how do I assign a transport when the same message class can represent totally different levels of urgency?

Another problem : A message should be able to have more than one priority

Enter: TransportNamesStamp and our custom PriorityStamp

Luckily, Symfony Messenger already has a built-in way to force a message to go to a specific transport: TransportNamesStamp. But to make things cleaner (and more semantic), let’s introduce our own PriorityStamp:

namespace App\Messenger\Stamp;

use Symfony\Component\Messenger\Stamp\StampInterface;

readonly class PriorityStamp implements StampInterface
{
    public function __construct(private string $priority) {}

    public function getPriority(): string
    {
        return $this->priority;
    }
}

And now, a custom middleware to hook into the dispatch flow:

readonly class PriorityRoutingMiddleware implements MiddlewareInterface
{
    public function handle(Envelope $envelope, StackInterface $stack): Envelope
    {
        // Check if the message has a PriorityStamp
        $priorityStamp = $envelope->last(PriorityStamp::class);

        if ($priorityStamp instanceof PriorityStamp) {
            $priority = $priorityStamp->getPriority();

            // Determine the transport based on priority
            $transport = match ($priority) {
                'high' => 'high_priority',
                'medium' => 'medium_priority',
                'low' => 'low_priority',
                'very_low' => 'very_low_priority',
                default => throw new \RuntimeException('Unknow priority level')
            };

            // Add a TransportNamesStamp to redirect the message
            $envelope = $envelope->with(new TransportNamesStamp([$transport]));
        }

        return $stack->next()->handle($envelope, $stack);
    }
}

Then, don’t forget to add our new custom middleware to the messenger configuration:

framework:
    messenger:
      # ...
      buses:
        messenger.bus.default:
          middleware:
            - 'App\Messenger\Middleware\PriorityRoutingMiddleware'

Now, when sending a message, I can easily override its priority — no need to create a new message class or refactor everything.

$this->messageBus->dispatch(
    new SendEmailMessage($notificationEmail),
    [new PriorityStamp('medium')],
)

In this case, we’re saying:

“Hey, this email isn’t that urgent — 10 minutes is fine.”

This keeps critical messages flowing fast, without flooding your high-priority queue with lower-stakes noise.

So where are we now?

✅ I can dynamically choose the transport

✅ I can adjust the priority at dispatch time

❌ I can ensure every message is handled within its max allowed time ← Still not there yet.

So… what’s the problem now?

Let’s say I send a message to update the price of every product variant in my catalog — or a large subset.

Not a big deal if I’ve got a few product variants. But if I have 100,000 variants? 500,000 variants? A million ? And each one makes a remote API call to fetch the price?

Here what happens:

📨 A message enters the queue.

🧠 It starts processing.

It takes 5, 10, 30… 60 minutes.

🧵 Meanwhile, one PHP worker is stuck.

Moone Boy watching his watch sitting on the ground with a suitcase next to himI could try to batch those API calls. Sure, that helps — but it only reduces the problem. It doesn’t solve it. Not in a way that truly scales.

And that’s a problem, even with all our beautiful prioritization logic — because long-running messages don’t play well in this model.

Last problem: Some messages take ages to be handled

Let’s say I have this message and corresponding handler in my project corresponding to the variant price update process:

readonly class UpdatePrices
{
    public function __construct(
        public array $filters,
    ) {
    }
}
#[AsMessageHandler]
readonly class UpdatePricesHandler
{
    public function __invoke(UpdatePrices $message): void
    {
        foreach ($this->productVariantRepository->findAllByRegex($message->filters) as $product) {
            $this->priceUpdater->updatePriceForVariant($variant);
        }
        
        $this->em->flush();
    }
}

Let’s be honest: the problem screams at us.

If 200,000 variants match this regex, and each update takes 0.1 second, that’s ~5 hours of processing in one go — way beyond our 1-hour limit.

To avoid jamming the queue, we’ll break this job into smaller messages. Let’s go to the extreme: one message = one variant update.

Keep the main message:

readonly class UpdatePrices
{
    public function __construct(
        public array $filters,
    ) {
    }
}

Add a unitary one:

readonly class UpdateVariantPrice
{
    public function __construct(
        public int $variantId,
    ) {
    }
}

Now change the handler to dispatch one message per variant:

public function __invoke(UpdatePrices $message): void
{
	foreach ($this->variantRepository->findByComplexQuery($message->filters) as $variant) {
        $this->messageBus->dispatch(new UpdateVariantPrice($product->getId()));
    }
}

Bonus: we can even plug in our dynamic priority logic:

public function __invoke(UpdatePrices $message): void
{
	foreach ($this->variantRepository->findByComplexQuery($message->filters) as $variant) {
    	if($variant->isSoldVeryOften()) {
        	$this->messageBus->dispatch(
            	new UpdateVariantPrice($variant->getId()),
            	[new PriorityStamp('medium')]
            );
        } else {
        	$this->messageBus->dispatch(new UpdateVariantPrice($variant->getId()));
        }
    }
}

And the new handler for the unitary message:

public function __invoke(UpdateVariantPrice $message): void
{
    $variant = $this->variantRepository->find($message->variantId);
    if (!$variant instanceof ProductVariant) {
        throw new UnrecoverableMessageHandlingException("Impossible to find the variant");
    }
    
    $this->priceUpdater->update($variant);
    $this->em->flush();
}

Sure, the total time may increase slightly, but the queue stays fluid. If a higher-priority message comes in, it’s picked up right away.

So what about now :

I can dynamically choose the transport

I can adjust the priority at dispatch time

I can ensure every message is handled within its max allowed time ← Still not there yet.

…Wait. Nothing changed?

Anakin Skywalker angry screaming "Liar"Let’s say I’ve sent:

  • 3 price updates

  • 10 exports

  • Hundreds of state updates

  • A few anonymizations

Do we need to scale?

Probably. But that’s a topic for another article — others are way more qualified than me to go deep on scaling strategies.

Still, here’s my two cents:

  • If you scale based on the number of pending messages, assign a weight per priority. (E.g. I count 1 high message as 1,200 very low ones.)

  • It won’t be perfect at first. Monitoring is your friend.

  • When downscaling, use hysteresis to avoid flapping between too many and too few workers.

Now we're talking

Now I can finally say it, for real:

I am able to prioritize dynamically and ensure that every message is handled during a given time period.

Prioritization cheat sheet

You want your messages to be processed in time? Prioritize.

  • Split your messages by priority: Define queues like high, medium, low, and very_low. Consume them in order.

  • Route dynamically with a stamp: Using TransportNamesStamp or a custom one.

  • Break big tasks into small ones: Don’t let a single message hog a worker for hours. Split it into smaller ones, and dispatch them.

In conclusion

Prioritizing messages in Symfony Messenger isn’t plug-and-play, but it’s not rocket science either. With a bit of planning, some custom code, and a mindset focused on time-to-handle (not just throughput), you can build a system where important things get done first — without choking the rest.

And once you’ve got that running? You’re not just dispatching messages anymore — you’re orchestrating flow.

Ready to optimize your Symfony application performance?

Implement message prioritization today with the team of the creator of Symfony and ensure your most critical tasks are always handled first.

This might also interest you

The SensioLabs team celebrating the 20th anniversary of Symfony with balloons
Jules Daunay

The Story Continues: SensioLabs Celebrates Symfony's 20th Anniversary

Time flies, especially when you're busy shaping the future of development! The SensioLabs team has just reached a milestone with the anniversary of the Symfony framework. We marked the occasion at the office, but the party isn't over yet. The date is already set for an XXL celebration at SymfonyCon Amsterdam 2025, from November 27 to 28.

Read more
PHP 8.5 URI extension
Oskar Stark

PHP 8.5's New URI Extension: A Game-Changer for URL Parsing

PHP 8.5 introduces a powerful new URI extension that modernizes URL handling. With support for both RFC 3986 and WHATWG standards, the new Uri class provides immutable objects, fluent interfaces, and proper validation - addressing all the limitations of the legacy parse_url() function. This guide shows practical before/after examples and explains when to use each standard.

Read more
Open in new tab
Silas Joisten

The Tab Trap: Why Forcing New Tabs Is Bad UX

We’ve all done it — added target="_blank" to a link to “help users” stay on our site. But what feels like a harmless convenience often creates confusion, breaks accessibility, and introduces hidden security risks.

Read more
3 dog heads
Mathieu Santostefano

Bring Your Own HTTP client

Break free from rigid dependencies in your PHP SDKs. Learn how to use PSR-7, PSR-17, and PSR-18 standards along with php-http/discovery to allow users to bring their favorite HTTP client, whether it's Guzzle, Symfony HttpClient, or another. A must-read for PHP and Symfony developers.

Read more
SensioLabs University Courses announcing the new level 3 Master training course now available
Jules Daunay

Master Symfony: Unlock Expert Skills with Our Training

Take your Symfony proficiency from good to great with the new Level 3 training course at SensioLabs! Master complex topics, optimize performance, and become a Symfony expert.

Read more
Two images: on the left many cars stuck in a traffic jam with the sign "All directions" above, on the right a blue car moving forward alone on the highway with the sign "Service Subscriber" and a Symfony logo above
Steven Renaux

Symfony Lazy Services with Style: Boost DX using Service Subscribers

Boost your Symfony app's performance and developer experience! Learn how to use Service Subscribers and traits for lazy service loading to reduce eager instantiation, simplify dependencies, and create modular, maintainable code.

Read more
Poster of Guillaume Loulier presentation
Salsabile El-Khatouri

A Symfony Training at SensioLabs: Behind The Scenes

What does Symfony training at SensioLabs look like? Find out in this interview with Guillaume Loulier, a passionate developer and trainer, who tells us all about the official Symfony training courses.

Read more
Domain Driven Design practical approach
Silas Joisten

Applying Domain-Driven Design in PHP and Symfony: A Hands-On Guide

Learn how to apply Domain-Driven Design (DDD) principles in Symfony with practical examples. Discover the power of value objects, repositories, and bounded contexts.

Read more
Photo speaker meetup AI Symfony
Jules Daunay

Symfony and AI: the video is now available

What about Symfony and Artificial Intelligence (AI)? This was the theme of the exclusive event organized by SensioLabs in partnership with Codéin on October 3rd. With the added bonus of feedback from a development project combining Symfony and AI. If you missed the event, check out the video now available for free on our Youtube channel.

Read more
2025 a year of celebrations for PHP with windows about API Platform, PHP, AFUP and Symfony
Jules Daunay

2025: a year of anniversaries for PHP, AFUP, Symfony and API Platform

2025 is going to be a big year for anniversaries. We will be celebrating the 20th anniversary of Symfony, the 30th anniversary of PHP, the 25th anniversary of AFUP and the 10th anniversary of API Platform. For SensioLabs, this is a major milestone that proves the longevity of the technologies in our ecosystem. We are proud to celebrate these anniversaries with the community all year long.

Read more
SymfonyDay Chicago 2025
Simon André

SymfonyDay Chicago 2025: A Celebration of Community

On March 17th, the Symfony community met in Chicago for SymfonyDay Chicago 2025. The event, held on St. Patrick's Day, was both a celebration of Symfony and a moment to support Ryan Weaver in his fight against cancer. It was more than just a conference — it was a gathering around a valued member of the community.

Read more
Blue ElePHPant on a computer
Imen Ezzine

Optimize Your PHP Code: 8 Functions You Need for Efficient Table Handling

If you want to become a good PHP developer, you must learn to work with arrays. Arrays are used a lot in PHP: temporarily store, organize, and process data before it's saved in a database. Knowing how to work with them efficiently will help you manage and process data more effectively.

Read more
type-safety-uuid
Oskar Stark

Type-Safe Identifiers with Symfony and Doctrine: Using Dedicated ID Classes

Learn how to enhance type safety in Symfony and Doctrine by using dedicated ID classes like BookId and UserId instead of raw UUIDs. This approach prevents identifier mix-ups, improves code clarity, and ensures better integration with Symfony Messenger and repository methods. Explore practical examples and best practices for implementing type-safe identifiers in your Symfony applications.

Read more
Grey Cargo Plane with a Blue Sky
Rémi Brière

Agility and the Cargo Cult - Part 1

Agility is more than just rituals and tools. In this first article of our Scrum series, we explore the Cargo Cult phenomenon and how blind imitation can hinder true Agile transformation.

Read more
SemVer vs. CalVer
Silas Joisten

SemVer vs. CalVer: Which Versioning Strategy is Right for You?

SemVer ensures stability for libraries, while CalVer aligns projects with release cycles. Learn the key differences and best use cases to optimize your versioning strategy.

Read more
Image