Message Broker

RabbitMQ

RabbitMQ ist ein AMQP Message Broker, der Nachrichten über Exchanges in Queues routet. Acknowledgements, Prefetch und Dead-Lettering machen es zu einem präzisen Werkzeug für Aufgaben, die nicht verloren gehen dürfen.

intermediate15 min readUpdated 16. Sept. 2026
publish.ts
ts
// publish.ts
import amqp from "amqplib";

const conn = await amqp.connect("amqp://localhost");
const ch = await conn.createChannel();

await ch.assertExchange("orders", "topic", { durable: true });
await ch.assertQueue("orders.created", { durable: true });
await ch.bindQueue("orders.created", "orders", "order.created.*");

ch.publish(
  "orders",
  "order.created.eu",
  Buffer.from(JSON.stringify({ id: 42 })),
  { persistent: true, contentType: "application/json" },
);

await ch.close();
await conn.close();
Veröffentlicht
2007
Protokoll
AMQP 0-9-1
Geschrieben in
Erlang
Kernidee
Exchanges und Bindings
Zustellung
At-least-once mit manuellem Ack
Ideal für
Routing, Work Queues, RPC

Warum es wichtig ist

Warum Teams zu RabbitMQ greifen

Routing als Feature

Direct-, Fanout-, Topic- und Headers-Exchanges routen eine Nachricht anhand von Keys, Patterns oder Attributen an viele Queues, statt dies manuell tun zu müssen.

Acknowledgements und Redelivery

Ein Consumer quittiert jede Nachricht mit einem Ack oder Nack. Eine nicht quittierte Nachricht wird bei Verbindungsabbruch erneut zugestellt, sodass Arbeit nicht stillschweigend verloren geht.

Integriertes Dead-Lettering

Abgelehnte oder abgelaufene Nachrichten können an einen Dead-Letter Exchange geroutet werden, was Retry-Queues und das Handling von Poison-Messages zu First-Class-Features macht.

Das Gesamtbild

Das AMQP-Mental-Model

Ein Producer veröffentlicht niemals direkt in einer Queue. Er veröffentlicht in einem Exchange, und Bindings entscheiden, welche Queues eine Kopie erhalten.

Exchange

Route

Producers veröffentlichen Nachrichten mit einem Routing Key in einem Exchange. Der Exchange entscheidet basierend auf seinem Typ und den Bindings, welche Queues eine Kopie erhalten.

Binding

Match

Ein Binding ist eine Regel, die einen Exchange mit einer Queue verbindet. Topic-Bindings nutzen Wildcards, Direct-Bindings matchen exakt.

Consumer

Acknowledge

Ein Consumer empfängt Nachrichten innerhalb eines Prefetch-Limits, verarbeitet jede einzelne und quittiert sie mit Ack, Nack oder schickt sie ins Dead-Lettering.

HTML5 auf einen Blick

Die Komponenten, die du konfigurierst

Queue

Ein geordneter Buffer, der Nachrichten hält, bis ein Consumer sie abruft.

Exchange

Das Routing-Eingangstor: direct, fanout, topic oder headers.

Binding

Eine Regel, die einen Exchange mit einer Queue verbindet, oft mittels eines Routing Keys.

Ack und Nack

Die manuelle Bestätigung teilt dem Broker mit, dass eine Nachricht sicher vergessen werden kann.

Prefetch

basic.qos begrenzt, wie viele nicht quittierte Nachrichten ein Consumer gleichzeitig halten darf.

Dead Letter

Abgelehnte, abgelaufene oder überlaufende Nachrichten werden an einen DLX geroutet.

Ablauf

Vom Publish zum Ack

Eine Nachricht wird erst dann aus ihrer Queue entfernt, wenn ein Consumer sie quittiert. Alles davor liegt in der Verantwortung des Brokers.

  1. 1

    In einen Exchange veröffentlichen

    Der Producer sendet eine Nachricht mit einem Routing Key und einem Delivery Mode. Er weiß nicht und muss nicht wissen, welche Queues existieren.

  2. 2

    Routing via Binding

    Der Exchange gleicht den Routing Key mit seinen Bindings ab und kopiert die Nachricht in jede passende Queue.

  3. 3

    Zustellung mit Prefetch

    Ein Consumer empfängt bis zu seinem Prefetch-Limit an nicht quittierten Nachrichten; dann pausiert der Broker die Zustellung, bis eine Nachricht quittiert wurde.

  4. 4

    Verarbeiten und Ack

    Der Handler schließt die Arbeit ab und ruft ack auf. Der Broker entfernt die Nachricht daraufhin endgültig aus der Queue.

  5. 5

    Nack oder Dead-Letter

    Bei einem Fehler quittiert der Consumer mit nack (inkl. Requeue) oder lehnt die Nachricht ab, sodass sie an einen Dead-Letter Exchange geroutet wird.

  6. 6

    Redeliver oder Parken

    Der Broker stellt eine nicht quittierte Nachricht nach einer Neuverbindung erneut zu oder parkt eine abgelehnte Nachricht in einer Dead-Letter Queue zur Inspektion.

Eine kurze Geschichte

Vom AMQP-Wagnis zum Enterprise-Broker

  1. 2007

    RabbitMQ wird erschaffen

    Rabbit Technologies baut einen AMQP Broker in Erlang und stellt ihn als Open Source zur Verfügung, in der Wette auf ein Standard-Protokoll.

    07
  2. 2010

    Übernahme durch SpringSource

    Der Broker erhält kommerzielle Unterstützung und wird zur Standard-Messaging-Schicht für viele Java- und Ruby-Stacks.

    10
  3. 2013

    Cluster und Federation

    Clustering und Federation reifen aus und ermöglichen es Teams, Broker über Rechenzentren und Vertrauensgrenzen hinweg zu betreiben.

    13
  4. 2018

    Streams werden eingeführt

    RabbitMQ 3.8 fügt einen log-ähnlichen Stream-Typ hinzu und übernimmt so das Replay-Modell von Kafka.

    18
  5. 2020

    Quorum Queues

    Raft-basierte Quorum Queues werden zur empfohlenen Replikationsstrategie anstelle von klassischen mirrored Queues.

    20
  6. 2023

    AMQP 1.0 Support

    RabbitMQ 3.12 und neuere unterstützen AMQP 1.0 neben 0-9-1, was das Client-Ökosystem erweitert.

    23

Der vollständige Leitfaden

RabbitMQ: Alles was Sie wissen müssen

Was ist RabbitMQ?

RabbitMQ ist ein Open-Source Message Broker, der AMQP (Advanced Message Queuing Protocol) spricht. Producer veröffentlichen Nachrichten darin, Consumer empfangen diese, und der Broker ist dafür verantwortlich, jede Nachricht so lange zu halten, bis sie von jemandem bestätigt wurde.

Die Formulierung, die das Design am besten beschreibt, ist ein „smart broker with dumb consumers“. RabbitMQ kennt sich mit Exchanges, Routing-Regeln, Acknowledgements, Retries und Dead Letters aus. Ein Consumer ist ein kleines Programm, das eine Nachricht liest, eine bestimmte Aufgabe erledigt und dann meldet, dass sie abgeschlossen ist. Diese Arbeitsteilung ist der Grund, warum RabbitMQ ideal für Systeme ist, in denen Routing und Zustellgarantien wichtiger sind als der reine Durchsatz.

Es wurde 2007 in Erlang geschrieben, weshalb es ungewöhnlich gut darin ist, viele gleichzeitige Verbindungen zu handhaben und einen Ruf für hohe Stabilität genießt. Es implementiert ein vollständiges Protokoll anstelle einer minimalen Queue-API – und genau dieses Protokoll ist sowohl die Quelle seiner Leistungsfähigkeit als auch seiner Lernkurve.

Das AMQP-Modell

Die wichtigste Grundidee in RabbitMQ ist, dass ein Producer niemals direkt an eine Queue veröffentlicht. Er veröffentlicht an einen Exchange, und der Exchange entscheidet, welche Queues eine Kopie erhalten.

producer -> exchange --binding--> queue -> consumer
                     \--binding--> queue -> consumer

Das Modell besteht aus fünf Komponenten:

  • Ein Producer öffnet einen Channel und ruft basic.publish mit einem Exchange-Namen, einem Routing Key und einem Body auf.
  • Ein Exchange empfängt jede veröffentlichte Nachricht und routet sie weiter. Er speichert nichts, es sei denn, er ist an eine Queue gebunden.
  • Ein Binding ist eine Regel, die einen Exchange mit einer Queue verbindet. Es kann einen Routing Key oder ein Pattern enthalten, und ein Exchange kann an viele Queues gebunden sein.
  • Eine Queue speichert Nachrichten in der richtigen Reihenfolge, bis ein Consumer sie abruft.
  • Ein Consumer abonniert eine Queue, empfängt die Nachrichten und bestätigt deren Erhalt (Acknowledgement).

Genau diese Indirektion ist der entscheidende Punkt. Ein Producer, der order.created.eu veröffentlicht, weiß nicht, ob ein Service, fünf Services oder überhaupt niemand zuhört. Neue Consumer werden hinzugefügt, indem eine neue Queue gebunden wird, ohne dass der Producer angepasst werden muss.

Die gesamte Arbeit findet auf einem Channel statt. Dabei handelt es sich um eine leichtgewichtige virtuelle Verbindung, die über eine einzige TCP-Verbindung multiplexed wird. Channels sind nicht thread-safe, daher ist das gängige Pattern ein Channel pro Task oder pro Consumer.

import amqp from "amqplib";

const conn = await amqp.connect(process.env.AMQP_URL!);
const ch = await conn.createChannel();

Exchange-Typen und Routing

Der Typ eines Exchanges bestimmt, wie ein Routing-Key mit seinen Bindings abgeglichen wird. Es gibt vier Typen, und jeder hat einen klaren Einsatzzweck.

Ein direct exchange routet an Queues, deren Binding-Key exakt mit dem Routing-Key übereinstimmt. Verwende ihn, um eine Nachricht an eine ganz bestimmte Queue oder an eine kleine Gruppe von Queues zu senden, die alle denselben Key teilen.

await ch.assertExchange("logs", "direct", { durable: true });
await ch.bindQueue("logs.errors", "logs", "error");
await ch.publish("logs", "error", body);

Ein fanout exchange ignoriert den Routing-Key vollständig und kopiert die Nachricht an jede gebundene Queue. Dies ist Pub/Sub in seiner reinsten Form: ein Publish, viele unabhängige Consumer.

await ch.assertExchange("events", "fanout", { durable: true });
await ch.bindQueue("search-indexer", "events", "");
await ch.bindQueue("email-notifier", "events", "");

Ein topic exchange gleicht den Routing-Key mit einem Pattern ab. Keys bestehen aus durch Punkte getrennten Wörtern. * matcht genau ein Wort und # matcht null oder mehr Wörter, was topic exchanges zum flexibelsten der vier Typen macht.

await ch.assertExchange("orders", "topic", { durable: true });

await ch.bindQueue("eu-orders", "orders", "order.*.eu");
await ch.bindQueue("all-orders", "orders", "order.#");
await ch.publish("orders", "order.created.eu", body);

Ein headers exchange ignoriert den Routing-Key und matcht stattdessen anhand von Attributen im Message-Header. Er ist selten die richtige Wahl, da topic exchanges leichter zu lesen und nachzuvollziehen sind, aber er ist nützlich, wenn das Routing von mehreren unabhängigen Attributen abhängt.

Wenn du dir eine Regel merkst, dann diese: Wähle den Exchange-Typ basierend auf der Frage, die du stellst. „Welche exakte Queue?“ ist direct. „Alle?“ ist fanout. „Welche Familie von Events?“ ist topic.

Acknowledgements, nack und prefetch

Die Zustellgarantie von RabbitMQ basiert auf Acknowledgements. Wenn ein Consumer eine Nachricht erhält, markiert der Broker diese als unacked, behält sie jedoch bei. Erst wenn der Consumer ack aufruft, wird die Nachricht entfernt. Bricht die Verbindung davor ab, stellt der Broker die Nachricht erneut zu.

await ch.consume("orders.created", async (msg) => {
  if (!msg) return;

  try {
    await handleOrder(JSON.parse(msg.content.toString()));
    ch.ack(msg);
  } catch (err) {
    ch.nack(msg, false, false); // requeue: false, dead-letter instead
  }
});

nack (oder das ältere reject) akzeptiert ein requeue-Flag. Ein Requeueing setzt die Nachricht wieder an den Anfang der Queue – das ist bei einem vorübergehenden Fehler richtig, aber falsch bei einer „Poison Message“, die immer wieder fehlschlagen wird. Die gängige Lösung ist hier, die Nachricht an eine Dead-Letter-Exchange zu senden.

Prefetch, konfiguriert über basic.qos, begrenzt die Anzahl der unbestätigten Nachrichten, die ein Consumer gleichzeitig halten kann. Ohne diese Einstellung pusht der Broker Nachrichten so schnell wie möglich, wodurch ein langsamer Consumer Tausende davon im Arbeitsspeicher puffert.

await ch.prefetch(20);

Setzen Sie den Prefetch-Wert auf ein kleines Vielfaches Ihrer tatsächlichen Concurrency. Ist der Wert zu hoch, blockiert ein einzelner Consumer die Queue; ist er zu niedrig, bleibt der Consumer untätig und wartet auf den nächsten Roundtrip.

Persistenz und Zustellgarantien

Eine Nachricht überlebt einen Neustart des Brokers nur, wenn drei Bedingungen erfüllt sind – und es ist leicht, eine davon zu übersehen.

  • Die Queue muss als durable: true deklariert werden.
  • Die Nachricht muss mit persistent: true veröffentlicht werden.
  • Der Exchange sollte ebenfalls durable: true sein.
await ch.assertExchange("orders", "topic", { durable: true });
await ch.assertQueue("orders.created", { durable: true });

ch.publish("orders", "order.created.eu", body, { persistent: true });

Bei der Persistenz geht es darum, einen Neustart zu überleben, nicht darum, die Zustellung zu garantieren. Dafür müssen Publisher Confirms aktiviert werden. Ohne Confirms ist publish ein „Fire-and-Forget“-Prozess: Wenn der Broker abstürzt, bevor die Nachricht geschrieben wurde, erfährt der Producer dies nie.

const ch = await conn.createConfirmChannel();

ch.publish("orders", "order.created.eu", body, { persistent: true }, (err) => {
  if (err) console.error("broker did not confirm", err);
  else console.log("message is safely queued");
});

Selbst mit Confirms und persistenten Queues ist die Zustellung At-Least-Once. Ein Consumer kann abstürzen, nachdem die Arbeit erledigt wurde, aber bevor der Ack gesendet wurde; in diesem Fall wird der Broker die Nachricht erneut zustellen. Exactly-Once über ein Netzwerk hinweg ist praktisch unmöglich, daher macht RabbitMQ diesen Kompromiss explizit und setzt voraus, dass Sie Ihre Handler idempotent gestalten.

Dead-letter exchanges und Retry-Queues

Ein dead-letter exchange (DLX) ist ein gewöhnlicher Exchange, der Nachrichten empfängt, die von einer Queue abgelehnt, abgelaufen oder verworfen wurden. Er ist der Mechanismus hinter Retry-Queues und dem Handling von Poison-Messages.

Eine Nachricht wird als Dead-Letter markiert, wenn eines der folgenden Ereignisse eintritt:

  • Der Consumer sendet einen nack oder lehnt die Nachricht mit requeue: false ab.
  • Die TTL (Time-to-Live) der Nachricht oder der Queue läuft ab.
  • Die Queue überschreitet ihr Längenlimit und verwirft die älteste Nachricht.

Sie konfigurieren den DLX an der Queue, in der die Arbeit liegt, nicht am Consumer.

await ch.assertQueue("orders.created", {
  durable: true,
  deadLetterExchange: "orders.dlx",
  deadLetterRoutingKey: "failed",
});

Das klassische delayed retry Pattern nutzt eine zweite Queue mit einer TTL und einem eigenen DLX, der zurück auf den Work-Exchange zeigt. Eine fehlgeschlagene Nachricht wird in die Retry-Queue verschoben (dead-lettered), verbleibt dort für die Dauer der TTL, läuft ab und wird anschließend wieder zurückgeschickt, um erneut verarbeitet zu werden. Dadurch wird ein Retry mit Verzögerung realisiert, ohne dass Sie Timer in Ihrem Code implementieren müssen.

await ch.assertExchange("orders.dlx", "direct", { durable: true });

await ch.assertQueue("orders.retry", {
  durable: true,
  messageTtl: 30_000,
  deadLetterExchange: "orders",
  deadLetterRoutingKey: "order.created.retry",
});

await ch.bindQueue("orders.retry", "orders.dlx", "retry");

Eine Nachricht, die kontinuierlich fehlschlägt, würde in einer Endlosschleife landen. Tracken Sie daher einen Retry-Counter in den Message-Headern und routen Sie die Nachricht nach Erreichen eines Limits in eine permanente failed Queue, die von niemandem automatisch konsumiert wird. Betrachten Sie diese Queue als operative Schnittstelle: Richten Sie Alarme ein, wenn sie anwächst, und implementieren Sie einen Pfad für den Replay.

Message TTL und Queue-Limits

Die Time-to-live (TTL) steuert, wie lange eine Nachricht warten darf. Sie kann pro Queue, pro Nachricht oder beides konfiguriert werden.

await ch.assertQueue("verification", {
  durable: true,
  messageTtl: 600_000,          // 10 minutes for every message
  maxLength: 10_000,            // keep at most 10k messages
  overflow: "reject-publish",   // backpressure instead of dropping
});

Die Per-Message TTL wird beim Publishen gesetzt und wird häufig für Werte verwendet, die sich von Nachricht zu Nachricht unterscheiden.

ch.publish("orders", key, body, {
  expiration: "30000", // milliseconds, as a string
});

Limits für die Queue-Länge dienen als Backpressure. maxLength begrenzt den Speicherverbrauch, und overflow legt fest, was passiert, wenn das Limit erreicht ist: drop-head verwirft stillschweigend die älteste Nachricht, während reject-publish neue Publishes ablehnt, sodass der Producer den Druck spürt. Für eine Queue, in der keine Arbeit verloren gehen darf, ist reject-publish der sicherere Standard und dient als Alarmsignal.

Work queues vs. pub/sub

Der gleiche Broker deckt zwei sehr unterschiedliche Kommunikationsmuster ab, und deren Verwechslung führt oft zu Bugs.

Eine work queue verteilt jede Nachricht an genau einen Consumer. Viele Worker konkurrieren um dieselbe Queue, und der Broker verteilt die Zustellungen per Round-Robin. Ergänzt man dies durch Prefetch und Acknowledgements, erhält man eine skalierbare, fehlertolerante Task-Queue.

await ch.assertQueue("jobs.thumbnails", { durable: true });
await ch.prefetch(5);

Pub/sub liefert jede Nachricht an jeden interessierten Consumer aus. Jeder Subscriber erhält seine eigene Queue, die an denselben Exchange gebunden ist, sodass ein langsamer oder offline befindlicher Subscriber niemals eine Nachricht von den anderen „stiehlt“.

await ch.assertExchange("events", "fanout", { durable: true });

await ch.assertQueue("events.billing", { durable: true });
await ch.bindQueue("events.billing", "events", "");

await ch.assertQueue("events.analytics", { durable: true });
await ch.bindQueue("events.analytics", "events", "");

Die Faustregel lautet: Eine Queue, die von vielen Consumern gemeinsam genutzt wird, ist eine work queue; eine Queue pro Consumer ist pub/sub. Ein Topic Exchange ermöglicht es, beides gleichzeitig zu nutzen, wobei einige Consumer eine Queue teilen und andere ihre eigene besitzen.

Das RPC-Pattern

RabbitMQ kann auch Request/Response-Kommunikation. Der Client veröffentlicht einen Request mit einer replyTo-Queue und einer correlationId, und der Server veröffentlicht die Antwort mit derselben ID an diese Queue.

import { randomUUID } from "node:crypto";

const { queue } = await ch.assertQueue("", { exclusive: true });
const correlationId = randomUUID();

ch.consume(queue, (msg) => {
  if (msg?.properties.correlationId === correlationId) {
    console.log("reply", JSON.parse(msg.content.toString()));
  }
}, { noAck: true });

ch.publish("rpc.inventory", "check", Buffer.from("{}"), {
  replyTo: queue,
  correlationId,
});

Die assertQueue("") erstellt eine temporäre, exklusive und automatisch gelöschte Queue nur für diesen Client. RPC über eine Queue ist nützlich, wenn der Aufrufer tatsächlich eine Antwort benötigt, führt jedoch wieder zu einer synchronen Kopplung und einem Timeout-Problem zurück. Für die meisten Systeme ist ein Event plus ein Callback-Event einfacher zu betreiben als RPC, und ein einfacher HTTP-Call ist noch einfacher, sofern die Abhängigkeit verfügbar ist.

Clustering und Quorum Queues

Ein einzelner Node stellt einen Single Point of Failure dar, weshalb RabbitMQ in Produktionsumgebungen als Cluster betrieben wird. Queues können über mehrere Nodes hinweg repliziert werden, und Clients verbinden sich bei einem Ausfall automatisch mit einem anderen Node neu.

Die moderne Replikationsstrategie ist die Quorum Queue, die auf dem Raft-Konsensalgorithmus basiert. Eine Quorum Queue besitzt einen Leader und mehrere Follower; ein Schreibvorgang wird erst dann bestätigt, wenn die Mehrheit der Nodes die Daten erhalten hat. Dies macht sie resistent gegen Netzwerkpartitionen, die bei klassischen mirrored queues zu Datenverlust führten, und macht sie zur heutigen Standardwahl für durable queues.

await ch.assertQueue("orders.created", {
  durable: true,
  arguments: { "x-queue-type": "quorum" },
});

Quorum Queues bevorzugen eine kleine, ungerade Anzahl an Replikaten, typischerweise drei oder fünf. Sie sind ressourcenintensiver als klassische Queues. Nutzen Sie sie daher für Daten, die nicht verloren gehen dürfen, und verwenden Sie für transiente Queues weiterhin die klassische Variante. Für Multi-Region-Topologien verschieben Federation und das Shovel-Plugin Nachrichten zwischen unabhängigen Brokern, anstatt einen einzigen Cluster über eine langsame Verbindung zu spannen.

Die Management-UI und das Monitoring

Jeder RabbitMQ-Node wird mit einem Management-Plugin ausgeliefert, das eine Web-UI und eine HTTP API bereitstellt. Dort werden Exchanges, Queues, Bindings, Connections und Channels angezeigt; zudem ist es möglich, Test-Nachrichten zu veröffentlichen oder Nachrichten aus einer Queue erneut abzuspielen.

rabbitmq-plugins enable rabbitmq_management

curl -u guest:guest http://localhost:15672/api/queues/%2F/orders.created

Auf einem Dashboard sind vor allem vier Kennzahlen entscheidend:

  • Queue depth — bereitstehende Nachrichten. Eine steigende Depth bedeutet, dass die Consumer nicht hinterherkommen.
  • Unacked count — zugestellte, aber noch nicht bestätigte Nachrichten. Eine Zahl, die stetig wächst, deutet darauf hin, dass die Consumer feststecken.
  • Publish- und Deliver-Rates — die Form des Traffics und ob die Consumer mit dem Tempo mithalten können.
  • Redelivery rate — eine steigende Anzahl an Redeliveries weist auf Abstürze oder wiederholte Fehler hin.

RabbitMQ liefert zudem Prometheus-Metriken, sodass Queue Depth und Consumer-Auslastung auf demselben Dashboard wie der Rest Ihrer Services erscheinen sollten. Richten Sie Alerts auf die Depth und das Alter der ältesten Nachricht ein, nicht auf einzelne Fehler, da diese zu erwarten sind.

Verbindungen, Channels und Recovery

Eine Verbindung ist kostspielig; ein Channel ist günstig. Öffnen Sie eine Verbindung pro Prozess und erstellen Sie dann einen Channel pro Producer, pro Consumer oder pro Arbeitseinheit. Ein Channel ist nicht thread-safe, daher führt das Teilen eines Channels über konkurrierende Handler hinweg zu verschachtelten Frames und verwirrenden Fehlern.

import amqp from "amqplib";

let conn: amqp.Connection;
let ch: amqp.Channel;

async function connect() {
  conn = await amqp.connect(process.env.AMQP_URL!);

  conn.on("error", (err) => console.error("connection error", err));
  conn.on("close", () => setTimeout(connect, 5_000));

  ch = await conn.createChannel();
  await ch.assertExchange("orders", "topic", { durable: true });
}

Behandeln Sie die Wiederverbindung (Reconnection) bewusst. amqplib übernimmt die Wiederverbindung nicht automatisch für Sie. Hören Sie daher auf close und bauen Sie die Verbindung, den Channel und jeden Consumer neu auf. Da eine unterbrochene Verbindung dazu führt, dass im Flug befindliche Nachrichten nicht als bestätigt (unacked) markiert werden, liefert der Broker diese erneut aus – genau das ist das gewünschte Verhalten. Dies ist eine weitere Stelle, an der die At-least-once-Delivery zum Tragen kommt: Eine Wiederverbindung kann Aufgaben erneut auslösen, daher müssen Handler idempotent sein.

Nachrichten-Eigenschaften und Prioritäten

Jede veröffentlichte Nachricht kann neben ihrem Body zusätzliche Eigenschaften mitführen. Diese reisen mit der Nachricht mit und sind für die Consumer sichtbar, was sie zu einem idealen Ort für leichtgewichtige Metadaten macht.

ch.publish("orders", "order.created.eu", body, {
  persistent: true,
  contentType: "application/json",
  messageId: order.id,
  correlationId: traceId,
  timestamp: Math.floor(Date.now() / 1000),
  type: "order.created",
  headers: { "x-retry-count": 0, "x-source": "checkout" },
});

contentType und messageId helfen den Consumern und dem Tooling; correlationId verknüpft eine Nachricht mit einem Trace; headers sind der Ort für Anwendungs-Metadaten, wie zum Beispiel einen Retry-Count. Platzieren Sie keine großen Datenmengen in den Headern, da der Broker diese indexieren und anzeigen muss.

RabbitMQ unterstützt zudem Priority Queues, bei denen Nachrichten mit höherer Priorität zuerst zugestellt werden. Deklarieren Sie die Queue mit einer maximalen Priorität und veröffentlichen Sie die Nachricht mit einem priority-Wert.

await ch.assertQueue("jobs", {
  durable: true,
  maxPriority: 10,
});

ch.publish("", "jobs", body, { priority: 9 });

Prioritäten sortieren nur Nachrichten neu, die bereits in der Warteschlange stehen. Wenn Consumer die Queue leer halten, bewirkt die Priorität nichts, und eine Nachricht mit hoher Priorität, die nach einer mit niedriger Priorität eintrifft, muss dennoch hinter dieser warten. Nutzen Sie Prioritäten für echte geschäftliche Unterscheidungen und nicht als allgemeinen Scheduling-Mechanismus.

RabbitMQ vs. Kafka: Die richtige Wahl

Die beiden werden oft verglichen, lösen jedoch unterschiedliche Probleme.

RabbitMQ ist ein Smart Broker für Routing und Work Queues. Nachrichten werden als Aufgaben betrachtet, die nach der Bestätigung (Acknowledgement) gelöscht werden. Exchanges routen Nachrichten nach Mustern, Bestätigungen erfolgen pro Nachricht und Dead-Lettering ist integriert. RabbitMQ ist ideal, wenn eine Nachricht eine bestimmte Gruppe von Queues erreichen muss oder wenn eine Arbeitseinheit bei einem Fehler erneut versucht und zwischengeparkt werden muss.

Kafka ist ein durable Log. Nachrichten werden an ein partitioniertes, geordnetes Log angehängt und für einen konfigurierten Zeitraum gespeichert, unabhängig davon, wer sie liest. Mehrere Consumer-Gruppen können dasselbe Topic unabhängig voneinander lesen, und jeder Consumer kann zu einem früheren Offset zurückspringen. Kafka glänzt bei extrem hohem Durchsatz und beim Event Replay.

Eine hilfreiche Faustregel: Wenn es ein Problem wäre, dass eine Nachricht konsumiert und gelöscht wurde, benötigen Sie ein Log. Wenn es Ihnen wichtig ist, dass eine Aufgabe korrekt geroutet und abgeschlossen wurde, benötigen Sie einen Broker. Viele Systeme setzen beides ein – Kafka für den Event Stream und RabbitMQ für die eigentliche Arbeit.

Best Practices

  • Deklariere Exchanges, Queues und Bindings beim Start idempotent und verwende durable: true für alles, was nicht verloren gehen darf.
  • Veröffentliche persistente Nachrichten in durable Queues und nutze Publisher Confirms für Aufgaben, die nicht verschwinden dürfen.
  • Bestätige (acknowledge) immer manuell, nachdem der Side Effect erfolgreich war, niemals vorher.
  • Setze ein Prefetch-Limit, damit ein einzelner Consumer nicht die gesamte Queue puffert.
  • Weise jeder durable Queue einen Dead-Letter Exchange zu und baue einen Replay-Pfad dafür.
  • Implementiere verzögerte Retries mit einer TTL-Retry-Queue, anstatt im Handler zu sleepen.
  • Gestalte Consumer idempotent, da die Zustellung “at-least-once” erfolgt.
  • Begrenze die Queue-Länge und wähle reject-publish, wenn das Verwerfen von Nachrichten nicht akzeptabel ist.
  • Nutze Quorum Queues für durable Daten und Classic Queues für transiente Daten.
  • Schließe Channels und Verbindungen bei SIGTERM und stoppe das Consuming vor dem Draining.
  • Überwache ready, unacked, die Redelivery-Rate und das Alter der ältesten Nachricht.

Häufige Fehler

  • Das Publishen in eine Queue statt in einen Exchange, nur um sich dann zu fragen, warum Bindings nicht funktionieren.
  • Auto-Acking (noAck: true), wodurch Nachrichten verloren gehen, sobald ein Handler abstürzt.
  • Das Vergessen von persistent: true, was zum Verlust von Nachrichten bei einem Broker-Neustart führt.
  • Eine Queue als durable zu deklarieren, aber non-persistent Nachrichten zu publishen.
  • Den Prefetch-Wert zu hoch anzusetzen, sodass ein einzelner Consumer alle anderen blockiert.
  • Eine Poison Message mit nack(msg, false, true) endlos zu requeuen.
  • Einen Dead-Letter Exchange ohne Consumer aufzubauen und diesen dann nie zu prüfen.
  • Einen Fanout Exchange zu verwenden, wo ein Topic Exchange nötig gewesen wäre, oder umgekehrt.
  • Einen einzelnen Broker in der Produktion zu betreiben und dies als hochverfügbar zu bezeichnen.
  • Work-Queue- und Pub/Sub-Semantiken in derselben Queue zu vermischen.
  • Den Event Loop in einem Consumer zu blockieren, was Heartbeats stoppt und eine fälschliche Trennung auslöst.

Wie geht es weiter?

Wenn Sie dasselbe Konzept für zuverlässige Hintergrundprozesse mit weniger Infrastruktur suchen, behandelt der Guide zu Redis Queues die Nutzung von BullMQ auf Basis von Redis. Für den allgemeineren Ansatz, Aufgaben aus dem Request-Pfad auszulagern, lesen Sie den Artikel über Batch Processing. Wenn Ihre Events ein dauerhafter Log sind, den viele Consumer erneut abspielen, anstatt Tasks, die geroutet und anschließend gelöscht werden, ist der Kafka-Guide der nächste logische Schritt. Die Node.js basics behandeln die Runtime, auf der jeder amqplib Consumer läuft.

In der Praxis

Vier RabbitMQ-Operationen

Publish, Consume, Routing per Topic und Dead-Lettering. Diese vier decken die Mehrheit der realen Deployments ab.

src/publish.ts
import amqp from "amqplib";

export async function publishOrder(order: Order) {
  const conn = await amqp.connect(process.env.AMQP_URL!);
  const ch = await conn.createChannel();

  await ch.assertExchange("orders", "topic", { durable: true });

  ch.publish(
    "orders",
    `order.created.${order.region}`,
    Buffer.from(JSON.stringify(order)),
    { persistent: true, contentType: "application/json" },
  );

  await ch.close();
  await conn.close();
}

Manueller Ack vs. Auto-Ack

Auto-Ack weist den Broker an, eine Nachricht sofort nach der Zustellung zu vergessen. Wenn der Handler abstürzt, ist die Arbeit verloren. Quittiere erst, wenn der Seiteneffekt erfolgreich war.

Bevorzugt
await ch.prefetch(20);

await ch.consume("orders.created", async (msg) => {
  if (!msg) return;
  try {
    await handleOrder(JSON.parse(msg.content.toString()));
    ch.ack(msg);
  } catch {
    ch.nack(msg, false, false);
  }
});
Vermeiden
// noAck: true means the broker deletes the message
// on delivery, before the handler has done anything.
await ch.consume("orders.created", async (msg) => {
  if (!msg) return;
  await handleOrder(JSON.parse(msg.content.toString()));
}, { noAck: true });

Topic Exchange vs. Direct Exchange

Ein Direct Exchange matcht einen Routing Key exakt. Ein Topic Exchange matcht Patterns, was ideal ist, wenn Consumer eine Familie von Events abonnieren.

Bevorzugt
await ch.assertExchange("events", "topic", { durable: true });

// One consumer gets every order event in every region.
await ch.bindQueue("audit", "events", "order.#");
await ch.publish("events", "order.created.eu", body);
Vermeiden
await ch.assertExchange("events", "direct", { durable: true });

// A direct binding only catches the exact key,
// so the audit consumer misses every other event.
await ch.bindQueue("audit", "events", "order.created.eu");
await ch.publish("events", "order.shipped.eu", body);

Abwägungen

Ist RabbitMQ der richtige Broker?

RabbitMQ ist ein intelligenter Broker für präzises Routing und zuverlässige Work Queues. Es ist kein Log und nicht die leichteste Lösung im Betrieb.

Strengths

  • Routing als First-Class-Citizen

    Exchanges, Bindings und Routing Keys routen eine Nachricht per Pattern an viele Queues, ohne dass der Producer wissen muss, wer sie konsumiert.

  • Nachvollziehbare, zuverlässige Zustellung

    Manuelle Acknowledgements, Publisher Confirms, Durable Queues und persistente Nachrichten ermöglichen eine At-Least-Once-Zustellung mit präzisen Stellschrauben.

  • Integriertes Dead-Lettering

    Retry-Queues, das Parken von Poison-Messages und verzögerte Retries werden deklarativ konfiguriert statt manuell programmiert.

Trade-offs

  • Kein Replayable Log

    Sobald eine Nachricht quittiert wurde, ist sie weg. Wenn Consumer Historien erneut lesen oder ab einem Offset replaying müssen, ist ein partitioniertes Log wie Kafka besser geeignet.

  • Aufwendiger im Betrieb

    Erlang-Runtime, Cluster-Bildung, Quorum Queues und Policy-Management bedeuten echten Aufwand. Managed Offerings mildern dies, entfernen es aber nicht.

  • Overhead pro Nachricht

    Acknowledgements und Confirms verursachen Roundtrips. Bei extremem Durchsatz gewinnt ein log-strukturierter Broker meist bei den reinen Kosten pro Nachricht.

Häufig gestellte Fragen

Häufig gestellte Fragen

Keep learning

Related topics from the roadmap.

$ Lernen Sie jetzt

Bereit, RabbitMQ zu lernen?

Unser interaktives Tutorial führt Sie Schritt für Schritt durch RabbitMQ — mit Quizzen und echtem Code, den Sie im Browser ausführen können.