Eine Benachrichtigung zu senden ist ein Funktionsaufruf. Eine Million zu senden ist ein Problem verteilter Systeme im Gewand eines Funktionsaufrufs.
Die naive Variante wirkt harmlos: Ein Nutzer löst ein Event aus, du iterierst über eine Million Empfänger und rufst für jeden den Push-, E-Mail- oder SMS-Provider auf. Im Demo funktioniert es. Bei tausend Nutzern funktioniert es. Dann fächert ein Produkt-Launch ein einziges Event über die gesamte Nutzerbasis auf, jeder Send wartet auf eine langsame Drittanbieter-API, und der Request-Thread, der in 50 ms hätte antworten sollen, hält jetzt eine Million offene Aufrufe. Die API stürzt ab — nicht weil der Code falsch ist, sondern weil die Form falsch ist.
Das hier ist ein anbieterneutraler Durchgang durch die Form, die hält: eine Message-Queue-Architektur mit Fan-out-Workern, Back-Pressure und den langweiligen Zuverlässigkeitsteilen, die verhindern, dass ein „Send“ zum Ausfall wird. Die Beispiele sind cloud-agnostisch — dasselbe Muster läuft auf RabbitMQ, Kafka, SQS oder Pub/Sub.
Der Fehler: Die Arbeit im Request erledigen
Als Erstes muss man reparieren, wo die Arbeit passiert. Wenn du im Request-Handler sendest, ist der Thread, der den HTTP-Aufruf angenommen hat, derselbe Thread, der auf einen langsamen externen Provider wartet — millionenfach.

Zwei Dinge gehen gleichzeitig schief. Erstens stapelt sich die Latenz: Die Antwortzeit deiner API ist jetzt die Summe aller Provider-Aufrufe, also zieht ein langsamer Anbieter den ganzen Request nach unten. Zweitens explodiert die Nebenläufigkeit: Eine Spitze reiht sich nirgends ein, sie öffnet eine Million gleichzeitige Sends gegen Provider, die dich bereitwillig rate-limiten oder abweisen, während dein eigener Connection-Pool und Speicher austrocknen.
Die Faustregel ist einfach:
Der Request soll den Auftrag annehmen, nicht ausführen.
Nimm das Event an, schreib es in eine dauerhafte Queue und antworte in Millisekunden mit 202 Accepted. Die schwere Arbeit wandert zu Hintergrund-Workern, die du unabhängig vom Request-Pfad skalieren, drosseln und wiederholen kannst. Die Spitze ist immer noch da — aber jetzt landet sie in einer Queue, wo ein Rückstau eine Unannehmlichkeit ist, kein Ausfall.
In „schreib es in eine dauerhafte Queue“ versteckt sich eine Feinheit. Schreibst du in deine Datenbank und publizierst in die Queue als zwei getrennte Schritte, verliert ein Absturz dazwischen das Event oder sendet ein Phantom. Die Lösung ist das transaktionale Outbox-Pattern: Schreib das Event in derselben Datenbank-Transaktion wie deinen fachlichen Write in eine outbox-Tabelle, und ein separater Relay befördert diese Zeilen in die Queue. Ein atomarer Write, keine verlorenen Events, keine doppelten Sends.
Der Kern: Eine Fan-out-Pipeline
Sobald die Arbeit vom Request-Pfad weg ist, wird die Architektur zur Pipeline. Ein Event kommt herein; kanalspezifische Benachrichtigungen gehen hinaus. Der Baustein, der sie skalieren lässt, ist Fan-out: eine einzige Ingress-Queue, aufgeteilt in je eine isolierte Spur pro Kanal.

Geh den Pfad durch:
- Producer → Ingress-Queue. Die API (oder ein vorgelagertes Event) publiziert pro logischem Event eine Nachricht in ein einziges dauerhaftes Topic. Das ist das Einzige, was der Request-Pfad berührt, und es ist schnell und append-only.
- Fan-out. Ein leichtgewichtiger Schritt liest das Ingress-Event, entscheidet welche Kanäle gelten (dieser Nutzer will Push + E-Mail, keine SMS) und publiziert eine Nachricht in die Queue jedes Kanals. Fan-out ist, wo aus „einem Event“ „drei Sends“ werden.
- Kanalspezifische Queues + Worker. Jeder Kanal — Push, E-Mail, SMS — hat seine eigene Queue und seinen eigenen Worker-Pool. Diese Isolation ist der ganze Punkt: Ein langsames SMS-Gateway staut nur die SMS-Queue. Push und E-Mail fließen weiter.
- Provider. Jeder Worker-Pool spricht mit seinem Provider (APNs/FCM, SMTP/SES, ein SMS-Gateway) in einem Tempo, das dieser Provider verkraften kann.
Die Isolation bringt drei Dinge, die ein einzelner geteilter Worker-Pool nicht kann: unabhängiges Skalieren (mehr E-Mail-Worker hochfahren, ohne SMS anzufassen), unabhängiges Scheitern (ein toter Provider staut die anderen nicht) und unabhängiges Tuning (E-Mail verträgt Minuten Verzögerung; Push oft nicht).
Zwei Hebel machen aus dem Diagramm ein Produktionssystem. Batche zum Provider: Die meisten Push-, E-Mail- und SMS-APIs akzeptieren Hunderte Empfänger pro Aufruf, also sendet ein Worker, der seine Queue zu Batches bündelt, weit mehr pro Sekunde als einer, der einzeln sendet. Und trenne nach Priorität, nicht nur nach Kanal: Ein transaktionaler OTP und eine Marketing-Blast dürfen sich nie eine Queue teilen — gib latenzkritischem Verkehr seine eigene Spur, damit eine Kampagne über eine Million Nachrichten keinen Login-Code verzögert. Wo die Reihenfolge pro Nutzer zählt, schlüssle diese Nachrichten auf dieselbe Partition, damit ein einzelner Worker sie der Reihe nach abarbeitet.
Anhand der Queue-Tiefe skalieren, nicht der Request-Rate
Die Worker sind der Ort, an dem eine Million Events tatsächlich versendet werden, und der Trick ist, worauf man skaliert. Skaliere nicht auf CPU oder Request-Rate — skaliere auf Queue-Tiefe und Nachrichtenalter. Wächst der Rückstau über einen Schwellenwert, füge Worker-Replicas hinzu; leert er sich, skaliere zurück.
Genau das entkoppelt Ankunftstempo von Sendetempo. Eine Million Events können in einer Minute eintreffen; deine Worker brauchen vielleicht eine Stunde zum Senden, gedrosselt auf das, was die Provider erlauben. Das ist in Ordnung — die Queue federt die Differenz ab. Die Aufgabe des Systems ist nicht, alles sofort zu senden. Sie ist, nie ein Event zu verlieren und nie abzustürzen, während so schnell abgebaut wird, wie die nachgelagerten Provider es zulassen.
Diese Worker-Pools laufen meist auf einer autoskalierenden Container-Plattform, und ist diese Plattform Kubernetes, zählt die Basis, auf der sie läuft, ebenso viel wie die Pipeline selbst — siehe Secure-by-Default GKE: Eine Referenzarchitektur für 2026.
Das ist dasselbe event-getriebene Rückgrat hinter jedem entkoppelten System. Baust du es speziell auf GCP, lohnt sich als Nächstes der Abgleich der Trade-offs zwischen Transport und Router: Pub/Sub oder Eventarc? Event-getriebenes GCP ohne Spaghetti.
Unter Last oben bleiben: Die langweiligen Teile, die zählen
Eine Queue und ein paar Worker bringen dich durchs Demo. Was einen Lauf über eine Million Nachrichten um 3 Uhr morgens am Leben hält, ist die Zuverlässigkeitsschicht — die Teile, die alle überspringen, bis die Produktion sie lehrt. Es sind vier, und sie funktionieren nur, weil du eine Queue hast.

Retry mit Backoff. Provider scheitern vorübergehend — ein Timeout, ein 503, ein kurzes Rate-Limit. Wiederhole, aber mit exponentiellem Backoff und Jitter, nie in einer engen Schleife. Eine enge Retry-Schleife über Tausende Worker ist von einem Denial-of-Service-Angriff nicht zu unterscheiden — und das Ziel ist dein eigener Provider. Backoff verteilt die Retries, sodass ein Aussetzer sich erholt, statt sich zu verstärken.
Dead-Letter-Queue (DLQ). Manche Nachrichten gelingen nie: ein fehlerhaftes Payload, eine dauerhaft ungültige Adresse, ein Bug. Verschiebe die Nachricht nach N Versuchen in eine Dead-Letter-Queue, statt sie endlos zu wiederholen. Die schlechte Nachricht wird zur Prüfung geparkt — nie verloren, nie die Linie blockierend — und der gesunde Verkehr dahinter läuft weiter. Ohne DLQ wiederholt sich eine Giftnachricht, bis sie die Queue aushungert.
Idempotenz. Jede dauerhafte Queue stellt mindestens einmal zu. Eine erneute Zustellung, ein langsames Ack, ein Worker, der nach dem Senden aber vor dem Ack abstürzt — jedes davon bedeutet, dass dieselbe Nachricht zweimal verarbeitet wird. Bedeutet „zweimal“ eine zweite E-Mail oder eine Doppelbelastung, ist das ein Bug, den du geschrieben hast. Dedupliziere auf einer stabilen Message-ID: notiere „Nachricht abc123 bereits gesendet“ und behandle einen erneuten Lauf als No-op. Halte diesen Eintrag in einem schnellen Store mit einer TTL, die dein Retry-Fenster abdeckt — du musst nur jüngste IDs merken, nicht die ganze Historie. Die erneute Zustellung wird harmlos.
Back-Pressure. Das ist der Schutz, der das gesamte System vom Kippen abhält. Jeder Worker respektiert ein Concurrency- und Rate-Limit pro Provider. Wird ein Provider langsamer, werden die Worker mit ihm langsamer — die Queue wächst, aber nichts stürzt ab. Back-Pressure verwandelt „der Provider hat Mühe“ in „der Rückstau ist heute tiefer“, statt „wir haben ihn gehämmert, bis er uns blockiert hat“.
Zusammengenommen sind diese vier der Grund, warum das Design seinen eigenen Erfolg überlebt. Die Queue macht sie möglich; überspringst du sie, wird deine Retry-Logik zu einem selbstverschuldeten DDoS gegen genau die Provider, von denen du abhängst.
Alles zusammengesetzt
Verfolge einen Launch durch die fertige Form. Eine Kampagne feuert ein Event. Die API schreibt es in die Ingress-Queue und antwortet in Millisekunden — der nutzerseitige Pfad weiß nicht einmal, dass eine Million Sends kommen. Fan-out teilt das Event in Push-, E-Mail- und SMS-Arbeit auf drei isolierten Queues. Jeder Worker-Pool skaliert auf seinem eigenen Rückstau, sendet im Tempo seines Providers, wiederholt vorübergehende Fehler mit Backoff, verschiebt die hoffnungslosen in die DLQ und dedupliziert erneute Zustellungen auf einer Message-ID. Die Spitze wird zu drei abbauenden Queues. Nichts stürzt ab.
Nichts davon ist exotisch. Es ist eine Queue, ein Fan-out-Schritt, Worker-Pools, die auf Tiefe skalieren, und vier Zuverlässigkeits-Schutzmechanismen. Der Fehler waren nie die Tools — es war, eine Aufgabe verteilter Systeme in einem einzigen Request-Thread zu erledigen. Nimm die Arbeit von diesem Thread, isoliere die Kanäle und lass Back-Pressure die Spitze abfedern — und „eine Million Benachrichtigungen senden“ hört auf, eine beängstigende Zahl zu sein, und wird zu einem Dienstag.
Die Feldregel
Nimm den Auftrag an, führ ihn nicht aus. Fan-out auf je eine Queue pro Kanal, damit Fehler lokal bleiben. Skaliere Worker auf Queue-Tiefe, nicht auf Request-Rate. Und behandle Retries, Dead-Letter-Queues, Idempotenz und Back-Pressure als tragend, nicht optional — sie sind der Unterschied zwischen einem Rückstau und einem Ausfall.





Aus der Community
Diskussion im Fediverse
Antworten von Mastodon und Bluesky — direkt aus dem offenen Netz, ohne Tracking.
Antworten werden geladen …
Noch keine Antworten. Starte die Diskussion:
Antworten konnten gerade nicht geladen werden.