Wie man Nachrichten beim Aufbau asynchroner Anwendungen mit dem Symfony-Messenger priorisiert

· Thibaut Chieux · Expertise · 7 Minuten zum Lesen
Blue sign on a building with several Now What? letters

Die asynchrone Verarbeitung bietet Vorteile wie entkoppelte Prozesse und schnellere Reaktionszeiten. Die Verwaltung von Nachrichtenprioritäten kann jedoch zu einer Herausforderung werden. Bei Aufgaben, die vom Zurücksetzen von Passwörtern bis hin zu komplexen Exporten reichen, ist die rechtzeitige Zustellung kritischer Nachrichten unerlässlich. Dieser Artikel befasst sich mit häufigen Problemen bei der asynchronen Verarbeitung und zeigt Lösungen mit Symfony Messenger auf, mit denen du deine Anwendung ohne umfangreiches Refactoring optimieren kannst.

Asynchronität klingt wie ein Traum: entkoppelte Prozesse, schnellere Reaktionszeiten und keine Benutzer mehr, die auf Ladebalken starren. Doch schnell holt einen die Realität ein – manche Nachrichten brauchen ewig, andere sind viel zu wichtig, um verzögert zu werden, und plötzlich versinkt man in einem Sumpf von Prioritäten.

Ganz gleich, ob du Passwörter zurücksetzt oder komplexe Exporte auslöst, du musst sicherstellen, dass die richtigen Nachrichten zur richtigen Zeit ankommen. Dieser Artikel beleuchtet die Probleme, denen du begegnen wirst – und wie du sie mit Symfony Messenger lösen kannst, ohne deine Anwendung von Grund auf neu zu schreiben oder um 3 Uhr morgens in deine Logs zu weinen.

Das Problem: Jede Nachricht dynamisch priorisieren

Animated gif with a lady with red glasses talking with the text "We have a priorities problem" belowWenn du beginnst, Nachrichten in deiner Symfony-Anwendung in eine Warteschlange zu stellen, wird eines schnell offensichtlich: Nicht alle Nachrichten sind gleich. Einige sind kritisch und zeitkritisch. Andere... eher weniger.

Einige Transportsysteme bieten bereits eine Möglichkeit, Prioritäten zu behandeln, wie zum Beispiel:

🐰 RabbitMQ hat x-priority

🌱 Beanstalkd hat eine integrierte Warteschlangenpriorität

Schön – aber was, wenn ich morgen zu einem anderen Transport wechseln möchte, ohne die Hälfte meines Codes neu zu schreiben?

Symfony Messenger bietet eine Lösung

Die offizielle Dokumentation zeigt, wie man Nachrichten basierend auf Priorität auf mehrere Transportsysteme aufteilt. Stell dir das wie das Zuweisen von Fahrspuren auf einer Autobahn vor: eine für Krankenwagen, eine für Roller.

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

💡 Hinweis: Die Warteschlangennamen kannst du selbst wählen – stell einfach sicher, dass sie deinen tatsächlichen Anwendungsfall widerspiegeln. Dies sind fiktive Beispiele.

Nachdem wir die Nachrichten nach Priorität aufgeteilt haben, ist das Konsumieren in der richtigen Reihenfolge genauso wichtig. Glücklicherweise macht Symfony Messenger das super einfach:

php bin/console messenger:consume async_priority_high async_priority_medium async_priority_low async_priority_very_low

Der Worker wird zuerst von async_priority_high konsumieren. Wenn diese leer ist, versucht er die nächste. Und so weiter. Selbst wenn es einen Rückstand an nicht dringenden Nachrichten gibt, bleiben Nachrichten mit hoher Priorität nicht dahinter hängen.

Wie wähle ich die richtige Warteschlange?

Das war ehrlich gesagt der kniffligste Teil für mich. Es gibt keine Patentlösung, und diese Tabelle ist entscheidend – alles andere in diesem Artikel hängt davon ab.

👉 Sie muss offensichtlich mit dem Kunden oder dem Produktverantwortlichen gefüllt werden und nicht nur mit dem Bauchgefühl des Entwicklers.

Die Frage, die du hier beantwortest, ist einfach, aber aussagekräftig:

„Was ist die maximal akzeptable Verzögerung (einschließlich Warteschlangenzeit und tatsächlicher Bearbeitung) für jede Art von Nachricht?“

Und daraus ergibt sich deine Prioritätszuordnung:

Priorität

Hoch

Mittel

Niedrig

Sehr niedrig

Bearbeitungszeit

1 Minute

10 Minuten

1 Stunde

1 Tag

Beispiel

- Status-Update
- E-Mail

- CMS-Update

- Preis-Update

- Analyse-Verarbeitung

- Export
- Anonymisierung

Nachdem ich die Nachrichten in Warteschlangen aufgeteilt habe, sollte alles perfekt sein, oder?

Nun ja… nicht ganz.

🙃 Ein Problem bleibt: Nachrichtentypen können zu generisch sein

Nehmen wir an, ich habe eine EmailMessage. Klingt gut. Aber ich könnte sie verwenden für:

  • Eine Passwortrücksetzung → 🟥 Hohe Priorität

  • Eine Lieferbenachrichtigung → 🟨 Mittlere Priorität

  • Einen „Bewerte deinen Kauf“-Ping → 🟦 Niedrige oder sehr niedrige Priorität

Wie weise ich also einen Transport zu, wenn dieselbe Nachrichtenklasse völlig unterschiedliche Dringlichkeitsstufen darstellen kann?

Ein weiteres Problem: Eine Nachricht sollte mehr als eine Priorität haben können

Enter: TransportNamesStamp und und unser benutzerdefinierter PriorityStamp

Hier kommen TransportNamesStamp und unser benutzerdefinierter PriorityStamp ins Spiel. Glücklicherweise bietet Symfony Messenger bereits eine integrierte Möglichkeit, eine Nachricht zu zwingen, zu einem bestimmten Transport zu gehen: TransportNamesStamp. Aber um die Dinge sauberer (und semantischer) zu gestalten, führen wir unseren eigenen PriorityStamp ein:

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;
    }
}

Und jetzt eine benutzerdefinierte Middleware, die sich in den Dispatch-Fluss einklinkt:

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);
    }
}

Vergiss dann nicht, unsere neue benutzerdefinierte Middleware zur Messenger-Konfiguration hinzuzufügen:

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

Beim Senden einer Nachricht kann ich jetzt einfach ihre Priorität überschreiben – ohne eine neue Nachrichtenklasse zu erstellen oder alles umzugestalten.

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

In diesem Fall sagen wir:

Hey, diese E-Mail ist nicht so dringend – 10 Minuten sind in Ordnung.

Dies hält kritische Nachrichten schnell im Fluss, ohne deine Hochprioritätswarteschlange mit weniger wichtigen Nachrichten zu überfluten.

Wo stehen wir jetzt?

✅ Ich kann den Transport dynamisch wählen

✅ Ich kann die Priorität zum Zeitpunkt des Versands anpassen

❌ Ich kann sicherstellen, dass jede Nachricht innerhalb ihrer maximal zulässigen Zeit bearbeitet wird ← Immer noch nicht erreicht.

Was ist denn jetzt das Problem?

Nehmen wir an, ich sende eine Nachricht, um den Preis jeder Produktvariante in meinem Katalog zu aktualisieren – oder eines großen Teils davon.

Kein großes Problem, wenn ich nur wenige Produktvarianten habe. Aber wenn ich 100.000 Varianten habe? 500.000 Varianten? Eine Million? Und jede einzelne macht einen Remote-API-Aufruf, um den Preis abzurufen?

Das passiert dann:

📨 Eine Nachricht gelangt in die Warteschlange.

🧠 Sie beginnt mit der Verarbeitung.

⏳ Es dauert 5, 10, 30… 60 Minuten.

🧵 Währenddessen steckt ein PHP-Worker fest.

Moone Boy watching his watch sitting on the ground with a suitcase next to himIch könnte versuchen, diese API-Aufrufe zu bündeln. Sicher, das hilft – aber es reduziert das Problem nur. Es löst es nicht. Nicht auf eine Weise, die wirklich skaliert.

Und das ist ein Problem, selbst mit unserer schönen Priorisierungslogik – denn lang laufende Nachrichten passen nicht gut in dieses Modell.

Letztes Problem: Manche Nachrichten brauchen ewig, bis sie bearbeitet werden

Nehmen wir an, ich habe diese Nachricht und den entsprechenden Handler in meinem Projekt, die dem Prozess der Variantenpreisaktualisierung entsprechen:

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();
    }
}

Seien wir ehrlich: Das Problem schreit uns an. Wenn 200.000 Varianten diesem Regex entsprechen und jede Aktualisierung 0,1 Sekunden dauert, sind das ~5 Stunden Verarbeitung auf einmal – weit über unser 1-Stunden-Limit hinaus.

Um ein Verstopfen der Warteschlange zu vermeiden, werden wir diesen Job in kleinere Nachrichten aufteilen. Gehen wir ins Extrem: eine Nachricht = eine Variantenaktualisierung.

Behalte die Hauptnachricht bei:

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

Füge eine unitäre hinzu:

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

Ändere nun den Handler, um eine Nachricht pro Variante zu versenden:

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

Bonus: Wir können sogar unsere dynamische Prioritätslogik einstecken:

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()));
        }
    }
}

Und der neue Handler für die unitäre Nachricht:

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();
}

Sicher, die Gesamtzeit kann leicht steigen, aber die Warteschlange bleibt flüssig. Wenn eine Nachricht mit höherer Priorität eingeht, wird sie sofort bearbeitet.

Was ist jetzt?

✅ Ich kann den Transport dynamisch wählen

✅ Ich kann die Priorität zum Zeitpunkt des Versands anpassen

❌ Ich kann sicherstellen, dass jede Nachricht innerhalb ihrer maximal zulässigen Zeit bearbeitet wird ← Immer noch nicht erreicht.

…Warte. Nichts hat sich geändert?

Anakin Skywalker angry screaming "Liar"Nehmen wir an, ich habe gesendet:

  • 3 Preis-Updates

  • 10 Exporte

  • Hunderte von Status-Updates

  • Einige Anonymisierungen

Müssen wir skalieren?

Wahrscheinlich. Aber das ist ein Thema für einen anderen Artikel – andere sind viel qualifizierter als ich, um tief in Skalierungsstrategien einzusteigen. Trotzdem meine zwei Cents:

  • Wenn du basierend auf der Anzahl der ausstehenden Nachrichten skalierst, weise jeder Priorität ein Gewicht zu. (Z.B. zähle ich 1 hochpriorisierte Nachricht als 1.200 sehr niedrigpriorisierte.)

  • Es wird am Anfang nicht perfekt sein. Monitoring ist dein Freund.

  • Beim Herunterskalieren verwende Hysterese, um ein Flackern zwischen zu vielen und zu wenigen Workern zu vermeiden.

Jetzt reden wir!

Jetzt kann ich es endlich wirklich sagen:

Ich bin in der Lage, dynamisch zu priorisieren und sicherzustellen, dass jede Nachricht innerhalb eines bestimmten Zeitraums bearbeitet wird.

Spickzettel zur Priorisierung

Du möchtest, dass deine Nachrichten pünktlich verarbeitet werden? Priorisiere.

  • Teile deine Nachrichten nach Priorität auf: Definiere Warteschlangen wie high, medium, low und very_low. Konsumiere sie der Reihe nach.

  • Route dynamisch mit einem „Stamp“: Verwenden TransportNamesStamp oder einen benutzerdefinierten.

  • Brich große Aufgaben in kleine auf: Lass eine einzelne Nachricht einen Worker nicht stundenlang blockieren. Teile sie in kleinere auf und versende diese.

Fazit

Das Priorisieren von Nachrichten in Symfony Messenger ist nicht Plug-and-Play, aber auch keine Raketenwissenschaft. Mit ein wenig Planung, etwas benutzerdefiniertem Code und einer Denkweise, die sich auf die Bearbeitungszeit (nicht nur den Durchsatz) konzentriert, kannst du ein System aufbauen, in dem wichtige Dinge zuerst erledigt werden – ohne den Rest zu ersticken.

Und wenn das erst einmal läuft? Dann versendest du nicht mehr nur Nachrichten – du orchestrierst den Fluss.

Möchtest du, dass deine Symfony-Anwendung besser läuft?

Mach jetzt die Nachrichten-Priorisierung mit dem Team des Schöpfers von Symfony. So stellst du sicher, dass deine wichtigsten Aufgaben immer zuerst erledigt werden.

Das könnte dich auch interessieren

Black green and golden computer RAM on a dark ground
Clément Bertillon

Gib deinen KI-Agenten ein dauerhaftes Gedächtnis: Das LLM-Wiki-Pattern in Symfony

Jeder Entwickler, der täglich Coding-Assistenten wie Claude Code, Codex oder Antigravity nutzt, kennt diesen Frust: Du startest einen Prompt für ein Projekt und musst – mal wieder – alles von vorne erklären. Der Agent hat keine Ahnung von deinen Symfony-Architektur-Präferenzen, deinen PHP-Standards, deiner DTO-Struktur, deinen Doctrine-Namenskonventionen oder den technischen Entscheidungen, die du letzte Woche in einem anderen Repository beschlossen hast. Am Ende verbringst du mehr Zeit damit, die Einleitung deines Prompts zu schreiben, als Features mit echtem Mehrwert zu programmieren.

Mehr erfahren : Gib deinen KI-Agenten ein dauerhaftes Gedächtnis: Das LLM-Wiki-Pattern in Symfony
Abstract editorial illustration of PHP 8.6 JSON parsing with a highlighted error position and subtle purple accents
Oskar Stark

PHP 8.6: JSON-Fehlerposition beim Decoding

PHP 8.6 bringt eine kleine, aber nützliche Verbesserung beim JSON-Decoding: Bei einem Parsing-Fehler lässt sich jetzt die exakte Fehlerposition ermitteln. Das beschleunigt das Debugging fehlerhafter Payloads deutlich.

Mehr erfahren : PHP 8.6: JSON-Fehlerposition beim Decoding
Large tree under the sunlight
Mathieu Santostefano

Vervielfache deine KI-Entwicklungsgeschwindigkeit mit Git Worktrees

Verabschiede dich vom lästigen „Stash-and-Switch“-Dilemma und vom Kontextwechsel. Durch die Kombination von Git Worktrees mit modernen KI-Agenten kannst du jetzt Umgebungen isolieren und Bugfixes parallel beheben, während deine KI im Hintergrund neue Funktionen entwickelt. Das ist ein absoluter Game-Changer für die Developer Experience (DX) und verwandelt deinen Git-Workflow in ein Multithreading-Kraftpaket.

Mehr erfahren : Vervielfache deine KI-Entwicklungsgeschwindigkeit mit Git Worktrees
A man sculpting a rock with PDF written on it
Steven Renaux

Create a Custom Builder – Eine GotenbergBundle Story

In einem vorherigen Artikel haben wir uns angeschaut, wie du mit nur wenigen Zeilen Code dein erstes PDF generierst. Dabei kamen Gotenberg und das GotenbergBundle zum Einsatz – ein Symfony Bundle, das die HTTP API von Gotenberg kapselt, um HTML- oder Office-Dateien in PDFs umzuwandeln oder Screenshots zu erstellen. Das war ein super Anfang. Aber was passiert, wenn deine Applikation viele verschiedene PDFs erzeugen muss, jedes mit eigenem Layout, Style und Daten?

Mehr erfahren : Create a Custom Builder – Eine GotenbergBundle Story
Nicolas Grekas standing on stage at SymfonyLive Paris 2026
Jules Daunay

SymfonyLive Paris 2026: KI-Revolution und Gipfeltreffen für das SensioLabs-Team

Der Vorhang für die SymfonyLive Paris 2026 ist gefallen, und wir haben immer noch Sterne ✨ (und Codezeilen) in den Augen. Als Entwickler von Symfony und langjähriger Hauptsponsor hätte sich SensioLabs keinen besseren Moment wünschen können, um Open Source, Innovation und vor allem die unglaubliche Community um uns herum zu feiern.

Mehr erfahren : SymfonyLive Paris 2026: KI-Revolution und Gipfeltreffen für das SensioLabs-Team
Paper notes on a wall
Imen Ezzine

Ein Blick hinter die Kulissen: 3 kollaborative Zeremonien

Nach einem Post auf LinkedIn dachte ich mir, ich schreibe diesen Artikel, um 3 Zeremonien zu beschreiben, die mich während eines meiner letzten Projekte besonders beeindruckt haben: Event Storming, Example Mapping und Domain Storytelling.

Mehr erfahren : Ein Blick hinter die Kulissen: 3 kollaborative Zeremonien
Why PHP?
Silas Joisten

Warum PHP das Unternehmensweb antreibt und warum diese strategische Entscheidung zählt

PHP gehört weiterhin zu den zuverlässigsten, skalierbarsten und kosteneffizientesten Backend Technologien für Unternehmenssysteme.

Mehr erfahren : Warum PHP das Unternehmensweb antreibt und warum diese strategische Entscheidung zählt
Symfony UX training
Elise Hamimi

Lerne Symfony UX mit dem neuen offiziellen Training von SensioLabs

In nur wenigen Jahren ist Symfony UX bei Symfony-Entwickler richtig beliebt geworden. Perfekt passend zu den heutigen Prioritäten, hilft es dir, interaktive und performante Interfaces zu bauen – ohne den Komfort des Frameworks zu verlassen. Zeit also, das Thema in unseren Schulungskatalog aufzunehmen. Genau deshalb starten wir offiziell unser neues Symfony-UX-Training.

Mehr erfahren : Lerne Symfony UX mit dem neuen offiziellen Training von SensioLabs