# Queues ohne Kafka: SQS, EventBridge und die eine Stelle, an der wir wirklich einen Stream brauchten

Jede Plattform erreicht den Moment, in dem eine Anfrage zu viel tut. Der Checkout-Handler verschickt eine E-Mail, aktualisiert den Suchindex, benachrichtigt das Lager, zeichnet ein Analytics-Ereignis auf und, ach ja, belastet auch noch die Karte. Das dauert vier Sekunden und scheitert, wenn eines der fünf Dinge langsam ist. Die Lösung ist eine Queue, und der erste Vorschlag lautet meist Kafka, weil Kafka das ist, was die großen Unternehmen benutzen, und die Konferenzvorträge von Kafka handeln.

Wir betreiben kein Kafka. Für eine Plattform, die wir für einen US-Kunden betreiben, nutzen wir SQS für Arbeit, EventBridge für Ereignisse und einen einzigen Kinesis-Stream für den einen Workload, der Reihenfolge und Replay brauchte. Hier ist, wie wir es aufgeteilt haben, was jedes kostet, und der Test dafür, ob Sie überhaupt einen Stream brauchen.

## Drei verschiedene Probleme

„Queue“ trägt in den meisten Architekturgesprächen eine Menge Last. Darunter verstecken sich drei Formen.

**Arbeit, die einmal von jemandem erledigt werden muss.** Diese E-Mail senden. Dieses Bild skalieren. Diese Bestellung mit dem Lager synchronisieren. Dem Erzeuger ist egal, wer es tut oder wann, nur dass es passiert, und einmal passiert. Das will eine *Queue*: Ein Element wird an einen Consumer geliefert, bei Erledigung bestätigt, sonst wiederholt, nach zu vielen Fehlern in die Dead-Letter-Queue geschoben.

**Etwas ist passiert, und jeder Interessierte sollte es wissen.** Eine Bestellung wurde aufgegeben. Ein Kunde hat seine E-Mail geändert. Der Erzeuger weiß nicht, wer zuhört, und sollte es nicht wissen müssen. Das will einen *Event-Bus*: einmal veröffentlichen, viele Abonnenten, jeder mit eigener Queue dahinter, hinzugefügt und entfernt, ohne den Erzeuger anzufassen.

**Eine geordnete, wiederabspielbare Historie.** Jede Änderung an diesem Konto, in Reihenfolge, die ein Consumer von jedem Punkt aus lesen und nach einem Bug erneut lesen kann. Das will einen *Stream*, und es ist die einzige der drei Formen, in der Kafka einzigartig gut ist. Es ist auch der seltenste Bedarf.

Die meisten Plattformen haben viel von der ersten, etwas von der zweiten und eine oder null von der dritten. Kafka kann alle drei, zum Preis, Kafka zu betreiben, oder für ein verwaltetes Kafka zu zahlen, das bei ein paar hundert Dollar im Monat beginnt, bevor Sie eine Nachricht gesendet haben.

<div class="article-figure">
<svg viewBox="0 0 900 260" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Drei Formen. Queue: Arbeit einmal von einem Consumer erledigt, Wiederholung und Dead-Letter, SQS, etwa 2 Dollar im Monat. Event-Bus: einmal veröffentlichen, viele Abonnenten je mit eigener Queue, EventBridge, etwa 1 Dollar. Stream: geordnet, wiederabspielbar, von jedem Offset lesbar, Kinesis, etwa 15 Dollar für einen Shard. Darunter: Kafka kann alle drei für ein paar hundert im Monat plus einen Operator.">
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<rect x="20" y="20" width="270" height="180" rx="12" fill="#151b2e" stroke="#4fffb0" stroke-width="1.5"/><text x="155" y="44" text-anchor="middle" fill="#4fffb0" font-weight="700">Queue · Arbeit</text><text x="155" y="70" text-anchor="middle" fill="#f1f3ff">tu das einmal, irgendjemand</text><text x="155" y="90" text-anchor="middle" fill="#f1f3ff">ein Consumer nimmt es</text><text x="155" y="110" text-anchor="middle" fill="#f1f3ff">Ack · Retry · Dead-Letter</text><text x="155" y="140" text-anchor="middle" fill="#9aa3c7" font-size="11">SQS · 11 Queues</text><text x="155" y="158" text-anchor="middle" fill="#9aa3c7" font-size="11">E-Mails, Bildskalierung, Lager-Sync</text><text x="155" y="184" text-anchor="middle" fill="#4fffb0" font-weight="700">~2 $ / Monat</text>
<rect x="315" y="20" width="270" height="180" rx="12" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/><text x="450" y="44" text-anchor="middle" fill="#7b8cff" font-weight="700">Event-Bus · Fakten</text><text x="450" y="70" text-anchor="middle" fill="#f1f3ff">das ist passiert</text><text x="450" y="90" text-anchor="middle" fill="#f1f3ff">einmal veröffentlichen, viele Abonnenten</text><text x="450" y="110" text-anchor="middle" fill="#f1f3ff">jeder bekommt seine eigene Queue</text><text x="450" y="140" text-anchor="middle" fill="#9aa3c7" font-size="11">EventBridge · 1 Bus · 9 Regeln</text><text x="450" y="158" text-anchor="middle" fill="#9aa3c7" font-size="11">OrderPlaced, CustomerUpdated, RefundIssued</text><text x="450" y="184" text-anchor="middle" fill="#7b8cff" font-weight="700">~1 $ / Monat</text>
<rect x="610" y="20" width="270" height="180" rx="12" fill="#151b2e" stroke="#ffd166" stroke-width="1.5"/><text x="745" y="44" text-anchor="middle" fill="#ffd166" font-weight="700">Stream · Historie</text><text x="745" y="70" text-anchor="middle" fill="#f1f3ff">geordnet pro Schlüssel, wiederabspielbar</text><text x="745" y="90" text-anchor="middle" fill="#f1f3ff">von jedem Punkt lesbar</text><text x="745" y="110" text-anchor="middle" fill="#f1f3ff">nach einem Bug erneut lesen</text><text x="745" y="140" text-anchor="middle" fill="#9aa3c7" font-size="11">Kinesis · 1 Stream · 1 Shard</text><text x="745" y="158" text-anchor="middle" fill="#9aa3c7" font-size="11">die Hauptbuch-Projektion, und nur die</text><text x="745" y="184" text-anchor="middle" fill="#ffd166" font-weight="700">~15 $ / Monat</text>
<text x="450" y="230" text-anchor="middle" fill="#9aa3c7">Kafka kann alle drei. Verwaltetes Kafka beginnt bei ein paar hundert im Monat; selbst gehostet beginnt bei einem Operator.</text>
<text x="450" y="248" text-anchor="middle" fill="#9aa3c7">Die meisten Plattformen brauchen viel vom ersten, etwas vom zweiten und eins oder null vom dritten.</text>
</g>
</svg>
</div>

## Queues: SQS

Elf SQS-Queues, eine pro Art von Arbeit, jede mit einer Dead-Letter-Queue und einem Lambda oder einem App-Runner-Worker, der sie konsumiert. Der Checkout-Handler, der vier Sekunden brauchte, schreibt jetzt die Bestellung, belastet die Karte (das Einzige, was synchron sein muss), veröffentlicht ein Ereignis und kehrt in 400 ms zurück. Alles andere ist ein Queue-Consumer.

Die Einstellungen, die zählen, in CDK:

```ts
const dlq = new sqs.Queue(this, 'EmailDlq', { retentionPeriod: Duration.days(14) });
const emailQueue = new sqs.Queue(this, 'EmailQueue', {
  visibilityTimeout: Duration.seconds(90),      // > 6 × das Timeout des Consumers
  deadLetterQueue: { queue: dlq, maxReceiveCount: 5 },
  encryption: sqs.QueueEncryption.SQS_MANAGED,
});
new lambda.EventSourceMapping(this, 'EmailConsumer', {
  target: emailFn, eventSourceArn: emailQueue.queueArn,
  batchSize: 10, reportBatchItemFailures: true,     // teilweiser Batch-Erfolg
  maxConcurrency: 20,                                // schützt den E-Mail-Anbieter
});
new cloudwatch.Alarm(this, 'EmailDlqAlarm', {
  metric: dlq.metricApproximateNumberOfMessagesVisible(), threshold: 1, evaluationPeriods: 1,
});
```

Drei Dinge, die in der ersten Version falsch waren und jetzt richtig sind. Das Visibility-Timeout war gleich dem Funktions-Timeout, also wurde eine langsame Nachricht erneut zugestellt, während sie noch verarbeitet wurde, und E-Mails gingen doppelt raus; jetzt ist es das Sechsfache des Funktions-Timeouts, wie die Doku sagt, die niemand liest. `reportBatchItemFailures` war aus, also ließ eine schlechte Nachricht in einem Zehnerbatch alle zehn scheitern, und neun gute E-Mails wurden je fünfmal wiederholt, bevor der Batch in der Dead-Letter-Queue landete. Und die Dead-Letter-Queue hatte keinen Alarm, also lagen Nachrichten eine Woche darin, bevor jemand hinsah; jetzt alarmiert sie bei eins.

**FIFO oder Standard?** Standard, überall außer beim Lager-Sync, bei dem „stornieren“ nach „anlegen“ für dieselbe Bestellung ankommen muss. FIFO mit der Bestell-ID als Message Group liefert das, zu etwa demselben Preis, mit einer Durchsatzgrenze pro Gruppe, von der wir weit entfernt sind. Setzen Sie FIFO nicht als Standard; es fügt einen Deduplizierungs- und Ordnungsvertrag hinzu, den die meiste Arbeit nicht will, und lässt eine hängende Nachricht alles hinter ihr in ihrer Gruppe blockieren.

## Ereignisse: EventBridge

Ein eigener Bus. Erzeuger legen Ereignisse mit `detail-type` und Quelle ab; Regeln matchen und leiten an Ziele weiter, die fast immer eine SQS-Queue im Besitz des konsumierenden Dienstes sind, mit einem Lambda dahinter. Der Erzeuger von `OrderPlaced` hat keine Ahnung, dass fünf Dienste es abonnieren, und wenn nächstes Quartal der sechste auftaucht, ist das eine neue Regel und eine neue Queue, ohne Änderung am Checkout.

```ts
const bus = new events.EventBus(this, 'PlatformBus');
new events.Rule(this, 'OrderPlacedToSearch', {
  eventBus: bus,
  eventPattern: { source: ['platform.orders'], detailType: ['OrderPlaced'] },
  targets: [new targets.SqsQueue(searchIndexQueue)],
});
new events.Rule(this, 'AllEventsToArchive', {
  eventBus: bus, eventPattern: { source: [{ prefix: 'platform.' }] },
  targets: [new targets.CloudWatchLogGroup(eventArchive)],   // 30 Tage, für „was ist passiert?“
});
```

Die Archivregel ist der billige Trick, der Ihnen das meiste von dem gibt, was Leute von einem Stream wollen: eine durchsuchbare, zeitlich geordnete Aufzeichnung jedes Ereignisses, [im selben Log-Speicher wie alles andere](/de/blog/your-logs-should-not-know-which-cloud), ohne Stream. Sie kann nicht in einen Consumer zurückspielen, aber sie kann „haben wir OrderPlaced für Bestellung 4412 ausgelöst?“ in einer Abfrage beantworten, und das ist die Frage, die tatsächlich gestellt wird.

EventBridges Vertrag ist mindestens-einmal, ungeordnet, mit einem Payload-Limit von 256 KB. Jeder Consumer ist idempotent, geschlüsselt auf die Ereignis-ID, und jedes Ereignis über ein paar KB trägt einen Zeiger auf S3, nicht die Payload. Diese zwei Regeln decken jedes Problem ab, das wir damit hatten.

## Der Stream: Kinesis, einmal

Das Hauptbuch. Jede finanzielle Bewegung auf der Plattform, in Reihenfolge pro Konto, projiziert in Salden und Berichte durch einen Consumer, der von Grund auf neu aufgebaut werden können muss, falls ein Projektionsfehler gefunden wird. Das ist das stream-förmige Problem: Reihenfolge pro Schlüssel und Replay von jedem Punkt.

Ein Kinesis-Stream, ein Shard, 7 Tage Aufbewahrung, ein Lambda-Consumer mit Checkpoint. Als im vierten Monat ein Projektionsfehler gefunden wurde, bestand die Lösung darin, den Consumer zu korrigieren, den Checkpoint auf den Beginn der Aufbewahrung zurückzusetzen und ihn drei Tage Ereignisse in eine frische Tabelle erneut lesen zu lassen. SQS kann das nicht; eine konsumierte Nachricht ist weg. EventBridge kann das nicht; das Archiv ist ein Log, kein Cursor.

<div class="article-figure">
<svg viewBox="0 0 900 240" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Wie sich eine Checkout-Anfrage verzweigt. Der Checkout-Handler schreibt die Bestellung und belastet die Karte synchron in 400 ms, dann veröffentlicht er ein OrderPlaced-Ereignis an EventBridge. Regeln leiten an fünf SQS-Queues: E-Mail, Suchindex, Lager-Sync FIFO, Analytics, Archiv-Log. Separat geht der Hauptbuch-Schreibvorgang in einen Kinesis-Stream, den die Saldenprojektion mit einem wiederabspielbaren Checkpoint liest.">
<defs><marker id="arrK" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#9aa3c7"/></marker></defs>
<g font-family="Inter,system-ui,sans-serif" font-size="11">
<rect x="20" y="70" width="170" height="90" rx="12" fill="#151b2e" stroke="#4fffb0" stroke-width="1.5"/><text x="105" y="94" text-anchor="middle" fill="#4fffb0" font-weight="700">Checkout · 400 ms</text><text x="105" y="114" text-anchor="middle" fill="#f1f3ff">Bestellung schreiben</text><text x="105" y="130" text-anchor="middle" fill="#f1f3ff">Karte belasten (sync)</text><text x="105" y="148" text-anchor="middle" fill="#9aa3c7">1 Ereignis veröffentlichen</text>
<line x1="192" y1="100" x2="258" y2="100" stroke="#9aa3c7" stroke-width="1.5" marker-end="url(#arrK)"/><text x="225" y="90" text-anchor="middle" fill="#9aa3c7" font-size="10">OrderPlaced</text>
<rect x="260" y="70" width="150" height="60" rx="12" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/><text x="335" y="96" text-anchor="middle" fill="#7b8cff" font-weight="700">EventBridge</text><text x="335" y="114" text-anchor="middle" fill="#9aa3c7">9 Regeln</text>
<g stroke="#9aa3c7" stroke-width="1.2" marker-end="url(#arrK)"><line x1="412" y1="100" x2="478" y2="30"/><line x1="412" y1="100" x2="478" y2="66"/><line x1="412" y1="100" x2="478" y2="100"/><line x1="412" y1="100" x2="478" y2="134"/><line x1="412" y1="100" x2="478" y2="168"/></g>
<g fill="#151b2e" stroke="#4fffb0"><rect x="480" y="16" width="200" height="28" rx="6"/><rect x="480" y="52" width="200" height="28" rx="6"/><rect x="480" y="86" width="200" height="28" rx="6"/><rect x="480" y="120" width="200" height="28" rx="6"/><rect x="480" y="154" width="200" height="28" rx="6"/></g>
<g fill="#f1f3ff" text-anchor="middle"><text x="580" y="35">SQS · E-Mail → Lambda</text><text x="580" y="71">SQS · Suchindex → Lambda</text><text x="580" y="105">SQS FIFO · Lager, nach Bestell-ID</text><text x="580" y="139">SQS · Analytics → Lambda</text><text x="580" y="173">CloudWatch Logs · Archiv, 30 T</text></g>
<line x1="192" y1="150" x2="258" y2="200" stroke="#ffd166" stroke-width="1.5" marker-end="url(#arrK)"/><text x="215" y="190" fill="#ffd166" font-size="10">Hauptbuch-Write</text>
<rect x="260" y="186" width="150" height="40" rx="12" fill="#151b2e" stroke="#ffd166" stroke-width="1.5"/><text x="335" y="211" text-anchor="middle" fill="#ffd166" font-weight="700">Kinesis · 1 Shard</text>
<line x1="412" y1="206" x2="478" y2="206" stroke="#ffd166" stroke-width="1.2" marker-end="url(#arrK)"/>
<rect x="480" y="190" width="200" height="32" rx="6" fill="#151b2e" stroke="#ffd166"/><text x="580" y="211" text-anchor="middle" fill="#f1f3ff">Saldenprojektion · wiederabspielbar</text>
<text x="790" y="100" text-anchor="middle" fill="#9aa3c7">jeder Consumer</text><text x="790" y="116" text-anchor="middle" fill="#9aa3c7">idempotent auf Ereignis-ID</text><text x="790" y="132" text-anchor="middle" fill="#9aa3c7">eigene DLQ, eigener Alarm</text>
<text x="790" y="200" text-anchor="middle" fill="#9aa3c7">die einzige Stelle,</text><text x="790" y="216" text-anchor="middle" fill="#9aa3c7">die Reihenfolge + Replay braucht</text>
</g>
</svg>
</div>

## Der Test für „brauchen Sie einen Stream?“

Fragen Sie: *Wenn ein Consumer letzten Dienstag einen Bug hatte, müssen Sie ihm die Nachrichten vom Dienstag in Reihenfolge erneut zuführen?* Wenn die ehrliche Antwort „wir würden einen Batch-Job gegen die Datenbank neu laufen lassen“ lautet, brauchen Sie eine Queue und eine Datenbank, keinen Stream. Wenn die Antwort „ja, und die Datenbank hat die Historie nicht“ lautet, brauchen Sie einen Stream, für diesen Consumer. Ein Stream für einen Consumer ist kein Grund, die ganze Plattform auf Kafka zu ziehen.

## Was es kostet

| | Monatlich | Betrieb |
|---|---|---|
| SQS, 11 Queues + 11 DLQs, ~4 Mio. Nachrichten | ~2 $ | Null. Alarme auf den DLQs |
| EventBridge, 1 Bus, 9 Regeln, ~1 Mio. Ereignisse | ~1 $ | Null. Archivregel für „was ist passiert“ |
| Kinesis, 1 Shard, 7 Tage Aufbewahrung | ~15 $ | Checkpoint-Überwachung; Shard-Anzahl, falls sie je relevant wird |
| **Gesamt** | **~18 $** | |
| Verwaltetes Kafka, kleinster sinnvoller Cluster | 300–600 $ | Topics, Partitionen, Consumer Groups, Aufbewahrung, eine Broker-Version zum Nachhalten |

Achtzehn Dollar und kein Broker. Die Plattform verarbeitet ein paar Millionen Nachrichten im Monat; SQS würde ein paar Milliarden in derselben Form verarbeiten, mit linear skalierender Rechnung und nichts, das neu zu architekturieren wäre.

## Die Kurzfassung

Teilen Sie „Queue“ in drei Probleme. Arbeit geht an SQS mit Dead-Letter-Queue und Alarm. Fakten gehen an EventBridge mit einer Queue pro Abonnent und einer Archivregel. Historie, wenn Sie wirklich einen Consumer haben, der in Reihenfolge wiederabspielen muss, geht in einen Kinesis-Stream, für diesen Consumer. Kafka ist die richtige Antwort, wenn Sie viel vom dritten Problem haben, und es ist in Ordnung, dieses Problem nicht zu haben.

Wenn Ihr Checkout vier Sekunden dauert, weil er fünf Dinge tut, [nehmen wir in einer Woche vier davon vom Anfragepfad](/contact), für etwa zwei Dollar im Monat.
