Idempotenz in verteilten Systemen, die tatsächlich funktioniert
Vermeiden Sie doppelte Seiteneffekte
Idempotenz in verteilten Systemen ist die Eigenschaft, die Sie rettet, nachdem das Netzwerk versagt hat, die Warteschlange erneut versucht, der Client in Panik gerät und der Betreiber eine Wiedergabe auslöst. In Produktionssystemen ist die mehrfache Zustellung normal. Mehrfache Seiteneffekte hingegen sind der Fehler.
HTTP definiert eine idempotente Methode als eine, bei der mehrere identische Anfragen dieselbe beabsichtigte Auswirkung auf den Server haben wie eine einzelne Anfrage. Aus diesem Grund sind PUT, DELETE und sichere Methoden gemäß den Protokollsemantiken idempotent und können nach einem Kommunikationsfehler automatisch erneut versucht werden.

Diese Definition ist nützlich, aber nicht ausreichend. In echten Architekturen ist Idempotenz keine Trivia-Antwort zu HTTP. Sie ist eine geschäftliche Garantie. Wenn ein Kunde einmal auf „Bezahlen“ klickt, dürfen Sie ihn nicht zweimal belasten, nur weil zwischen dem Commit und der Antwort ein Timeout aufgetreten ist. Wenn ein Worker den Inventarbestand aktualisiert und abstürzt, bevor er die Nachricht bestätigt (acknowledged), dürfen Sie den Lagerbestand nicht zweimal dekrementieren, nur weil der Broker die Nachricht erneut zugestellt hat. Das ist der Maßstab.
Der Fehler, den ich immer wieder sehe, ist die Behandlung von Idempotenz als Transportfeature anstatt als Systemeigenschaft. Queue-Entdopplung, HTTP-Verben und Client-Neuversuche helfen, aber keines davon rettet ein Design, das dasselbe geschäftliche Intent (Absicht) erlaubt, einen zweiten Seiteneffekt zu erzeugen. Wenn Sie den breiteren Rahmen dafür sehen möchten, wie diese Integrationsentscheidungen zu Service-Grenzen und Persistenz-Trade-offs passen, beginnen Sie mit App-Architektur in der Produktion: Integrationsmuster, Code-Design und Datenzugriff.
Woher Duplikate in der Produktion stammen
Duplikate entstehen nicht, weil Teams sorglos sind. Sie entstehen, weil verteilte Systeme neu versuchen, neu ordnen und wiedergeben.
Ein Client kann eine Erstellungsanfrage senden, der Server kann sie committen, und die Antwort kann trotzdem auf der Leitung verschwinden. Genau deshalb unterscheidet HTTP idempotente Methoden und warum Payment-APIs wie Stripe und PayPal explizite Idempotenzmechanismen für unsichere Methoden wie POST bereitstellen.
Nachrichtenbroker machen das Problem noch offensichtlicher. Zustellung mit mindestens einmaliger Garantie (at-least-once delivery) bedeutet, dass ein Consumer für dieselbe Nachricht wiederholt aufgerufen werden kann. Ein Handler kann die Datenbank erfolgreich aktualisieren, aber vor der Bestätigung fehlschlagen, wodurch der Broker dieselbe Nachricht erneut zustellt.
Webhooks sind nicht anders. GitHub gibt an, dass Webhook-Zustellungen in falscher Reihenfolge eintreffen können, fehlgeschlagene Zustellungen nicht automatisch erneut zugestellt werden und jede Zustellung einen einzigartigen X-GitHub-Delivery-GUID enthält, den Sie beim Schutz vor Replay-Angriffen verwenden sollten. Für eine praktische Architekturperspektive auf Chat-Endpunkte als Interaktionsgrenzen siehe Chat-Plattformen als Systemschnittstellen in modernen Systemen.
Selbst Systeme, die stärkere Garantien werben, lassen Ihnen noch Arbeit übrig. Kafka kann doppelte Einträge in Kafka-Logs mit idempotenten Produzenten verhindern und kann genau-einmal-Zustellung (exactly-once delivery) für Read-Process-Write-Flows bieten, die innerhalb von Kafka mit Transaktionen und read_committed-Consumers bleiben. Aber Kafkas eigene Design-Dokumente sind klar, dass externe Systeme immer noch eine Koordination mit Offsets und Ausgaben benötigen. Google Cloud Pub/Sub genau-einmal-Zustellung ist auf Pull-Abonnements beschränkt, innerhalb einer Cloud-Region und erfordert immer noch, dass Clients den Fortschritt der Verarbeitung verfolgen, bis die Bestätigung erfolgreich ist.
Meine gefällige Zusammenfassung ist einfach. Gehen Sie davon aus, dass der Transport neu versuchen wird. Gehen Sie davon aus, dass Betreiber Wiedergaben auslösen werden. Gehen Sie davon aus, dass Webhooks verspätet eintreffen werden. Entwerfen Sie den Schreibpfad so, dass ein wiederholtes Intent keinen zweiten Geschäftseffekt erzeugen kann.
Der API-Vertrag, dem ich tatsächlich vertraue
Wie verhindern Idempotenzschlüssel doppelte API-Anfragen
Der einzige API-Vertrag, dem ich für mutierende Operationen vertraue, ist vom Aufrufer bereitgestellter Intent plus serverseitige Persistenz.
AWS empfiehlt eine vom Aufrufer bereitgestellte Anfrage-Identifikation und warnt davor, dass der Dienst das Idempotenz-Token atomar zusammen mit der mutierenden Arbeit aufzeichnen muss. Stripe speichert den ersten Statuscode und Antwortkörper für einen Schlüssel, vergleicht spätere Parameter mit der ursprünglichen Anfrage und gibt dasselbe Ergebnis für Neuversuche zurück. PayPal verwendet PayPal-Request-Id auf unterstützten POST-APIs und gibt den neuesten Status für die vorherige Anfrage mit diesemselben Header zurück.
Das führt zu einem praktischen Vertrag:
- Der Client generiert einen Idempotenzschlüssel für eine Geschäftsvorgang.
- Der Server grenzt diesen Schlüssel nach Tenant und Operationsnamen ein.
- Der Server speichert einen Request-Hash, sodass derselbe Schlüssel nicht für eine andere Payload wiederverwendet werden kann.
- Der Server zeichnet Zustände wie
pending(ausstehend),completed(abgeschlossen) oderfailed(fehlgeschlagen) auf. - Neuversuche mit demselben Schlüssel geben entweder das gespeicherte Ergebnis zurück oder einen stabilen Pointer darauf.
- Neuversuche mit demselben Schlüssel und einer anderen Payload scheitern lautstark.
Es gibt einen IETF-Entwurf für den Idempotency-Key-Header, aber Stand 09.05.2026 ist er im IETF Datatracker immer noch als abgelaufener Internet-Draft aufgeführt, nicht als veröffentlichte RFC. In der Praxis ist der Header-Name weiterhin als de-facto-Konvention weit verbreitet nützlich, aber Sie sollten den Vertrag in Ihrer eigenen API dokumentieren, anstatt so zu tun, als wäre der Standard abgeschlossen.
Was sollte der Schlüssel repräsentieren? Intent (Absicht). Nicht einen HTTP-Versuch. Nicht eine TCP-Verbindung. Nicht einen Retry-Counter. Wenn der Benutzer „Bestellung 123 einmal erstellen“ meint, muss jeder Neuversuch für diesenselben Befehl denselben Schlüssel wiederverwenden. Wenn der Benutzer „eine zweite Bestellung aufgeben“ meint, muss das einen anderen Schlüssel verwenden.
Eine Request-ID ist für Tracing gedacht. Ein Idempotenzschlüssel ist für Korrektheit. Wenn Sie diese verwechseln, sehen Ihre Dashboards ordentlich aus, während Ihr Geld zweimal bewegt wird.
Warum PUT nicht ausreicht
Nein, HTTP-PUT reicht nicht aus, um eine Operation idempotent zu machen.
Ja, RFC 9110 gibt PUT idempotente Semantiken. Aber wenn Ihr PUT-Handler ein neues Downstream-Event auslöst, bei jedem Neuversuch eine E-Mail sendet oder einen externen Anbieter erneut belastet, dann hat Ihre Implementierung den Geschäftsvertrag verletzt, auch wenn Ihr Routenname respektabel aussieht.
Die Wahl des Verbs hilft Clients, den Intent zu verstehen. Es implementiert den Intent nicht für Sie.
Verwenden Sie PUT, wenn das Ressourcenmodell tatsächlich eine vollständige Ersetzung oder eine Upsert-ähnliche Operation passt. Verwenden Sie POST, wenn Sie Befehle oder Aktionen erstellen. Aber für jede Mutation, die über Netzwerkgrenzen hinweg erneut versucht werden könnte, dokumentieren Sie einen expliziten Idempotenzvertrag. Wenn Ihre mutierenden Aktionen aus Chat-Workflows ausgelöst werden, gilt derselbe Vertrag in Slack-Integrationsmuster für Alerts und Workflows und Discord-Integrationsmuster für Alerts und Kontrollschleifen. Versteckte Seiteneffekte sind der Ort, an dem Architektur stirbt.
Wie lange sollte ein Idempotenzschlüssel gespeichert werden
Länger, als es Ihr Transport-Team möchte.
Stripe sagt, dass Schlüssel nach mindestens 24 Stunden gelöscht werden können. PayPal sagt, dass die Aufbewahrung API-spezifisch ist und Beispiele gibt, die bis zu 45 Tage dauern können. Amazon SQS FIFO dupliziert nur innerhalb eines 5-Minuten-Fensters. GitHub behält最近的 Zustellungen für 3 Tage bei, um manuelle Neuzustellung zu ermöglichen. Diese Zahlen sind wildly unterschiedlich, weil die richtige Aufbewahrungsfrist eine geschäftliche Entscheidung ist, kein Protokollstandardwert.
Wenn Sie Schlüssel nur fünf Minuten lang behalten, weil es Ihre Queue tut, entwerfen Sie keine Idempotenz. Sie kopieren eine Transportbegrenzung in Ihre Geschäftsschicht.
Behalten Sie Idempotenzdatensätze mindestens für das Maximum dieser Fenster:
- Client-Neuversuchs-Horizont
- Queue-Redrive-Horizont
- Webhook-Replay-Horizont
- Betreiber-Replay-Horizont
- Settlement- oder Kompensationshorizont für geldbewegende Operationen
Für Zahlungen, Buchungen und Provisionierung bedeutet das oft Stunden oder Tage, nicht Minuten.
AWS nennt auch zwei Anti-Pattern, mit denen ich voll einverstanden bin. Verwenden Sie keine Zeitstempel als Schlüssel, weil Uhrenversatz und Kollisionen sie unzuverlässig machen. Speichern Sie nicht blindweise gesamte Anfrage-Payloads als Entduplicierungsdatensatz für jede Anfrage, weil das Leistung und Skalierbarkeit beeinträchtigt. Speichern Sie einen normalisierten Request-Hash plus den minimalen Antwortzustand, den Sie für ein sicheres Replay benötigen. Wenn Sie den ersten Antwortbyte für Byte reproduzieren müssen, speichern Sie den kanonischen Antwortkörper, wie es Stripe tut.
Die Datenbankmuster, die Idempotenz real machen
Idempotenz wird real, wenn die Persistenzschicht ein Rennen genau einmal gewinnen kann.
PostgreSQL gibt Ihnen hier zwei kritische Primitive. Unique Constraints (Einzigartigkeitsbeschränkungen) erzwingen Einzigartigkeit auf einer oder mehreren Spalten, und INSERT ... ON CONFLICT lässt Sie eine alternative Aktion definieren, anstatt bei einer Einzigartigkeitsverletzung zu scheitern. PostgreSQL dokumentiert auch, dass ON CONFLICT DO UPDATE ein atomares Insert-or-Update-Ergebnis unter Konkurrenz garantiert.
Das bedeutet, dass Ihre Idempotenzschicht normalerweise mit einer Tabelle wie dieser beginnen sollte:
create table api_idempotency (
tenant_id text not null,
operation text not null,
idempotency_key text not null,
request_hash text not null,
state text not null,
status_code integer,
response_body jsonb,
resource_type text,
resource_id text,
created_at timestamptz not null default now(),
expires_at timestamptz not null,
primary key (tenant_id, operation, idempotency_key)
);
Und der Handlings-Flow sollte so aussehen:
begin transaction
try insert (tenant_id, operation, idempotency_key, request_hash, state='pending')
on conflict do nothing
load row for (tenant_id, operation, idempotency_key) for update
if row.request_hash != incoming_request_hash
fail with conflict or validation error
if row.state = 'completed'
return stored response
if row.state = 'pending' and row was created by another live request
either wait briefly, or fail fast with a retryable response
perform local business mutation
store stable result in idempotency row
set state = 'completed'
commit
return result
Der wichtige Teil ist nicht die Syntax. Der wichtige Teil ist die Atomicität. Das Aufzeichnen des Schlüssels und das Durchführen der Mutation müssen zusammen erfolgreich sein oder zusammen fehlschlagen. AWS sagt dies explizit für API-Idempotenz, und dieselbe Regel gilt für SQL-gestützte Dienste.
Führen Sie keine naive Check-then-Act-Sequenz wie „select key; if missing then insert order“ durch. Unter Konkurrenz können zwei Anfragen den Check bestehen und beide den Seiteneffekt erzeugen. Eine Unique-Constraint ist nicht optional. Es ist der Mechanismus, der Ihre Architektur von optimistischem Volkswissen in etwas verwandelt, das Sie unter Last beweisen können.
Hier ist die Regel, die ich in Reviews verwende. Wenn die Entduplicierungsentscheidung nicht durch dieselbe transaktionale Grenze geschützt ist wie die Mutation, haben Sie keine Idempotenz. Sie haben Hoffnung.
Nachrichten, Events und Webhooks benötigen ihre eigene Grenze
Wie gehen Consumer mit doppelten Events und Nachrichten um
Für Message-Consumer ist das klassische Muster immer noch das richtige. Speichern Sie verarbeitete Nachrichten-IDs in derselben Datenbanktransaktion wie die Geschäftsupdates. Chris Richardson beschreibt den Ansatz der PROCESSED_MESSAGES-Tabelle direkt, wobei ein Primary Key auf Subscriber und Nachrichten-ID verwendet wird, sodass Duplikate sauber scheitern und ignoriert werden können.
Viele Teams nennen diesen expliziten processed_messages-Store eine Inbox-Tabelle. Die Bezeichnung ist weniger wichtig als die Regel. Der Empfänger muss den Beweis persistent speichern, dass er die Nachricht bereits verarbeitet hat, bevor ein Neuversuch sicher nichts tun kann.
Eine minimale Form sieht so aus:
create table processed_messages (
subscriber_id text not null,
message_id text not null,
processed_at timestamptz not null default now(),
primary key (subscriber_id, message_id)
);
Und der Consumer-Flow ist genauso streng wie der HTTP-Flow:
begin transaction
insert into processed_messages (subscriber_id, message_id)
values (?, ?)
on conflict do nothing
if no row inserted
rollback
ack and ignore duplicate
apply business mutation
commit
ack message
Dieses Muster ist langweilig. Gut. Idempotenz sollte langweilig sein.
Es ist auch normalerweise besser, als sich auf Marketingbegriffe von Brokern zu verlassen. Kafkas genau-einmal-Unterstützung ist exzellent, wenn Sie innerhalb von Kafkas eigenem transaktionalem Modell bleiben, aber Kafkas Dokumente warnen immer noch, dass externe Ziele Kooperation benötigen. SQS FIFO reduziert doppelte Sendungen nur innerhalb seines 5-Minuten-Entduplicierungsfensters. Pub/Sub genau-einmal erwartet immer noch, dass der Subscriber den Fortschritt verfolgt und doppelte Arbeit vermeidet, wenn Bestätigungen fehlschlagen.
Genau-einmal ist normalerweise eine lokale Optimierung. Idempotente Seiteneffekte sind die Systemgarantie.
Kombinieren Sie Entduplicierung mit dem Outbox-Muster
Wenn Ihr Dienst lokalen Zustand aktualisiert und auch ein Event veröffentlicht, reicht idempotenter Konsum allein nicht aus. Sie benötigen auch einen sicheren Weg, das Event nach dem Commit der lokalen Transaktion herauszubekommen.
Deshalb ist das transaktionale Outbox-Muster wichtig. Chris Richardson beschreibt die Grundidee als Schreiben des Events in eine Outbox-Tabelle in derselben Transaktion wie die Geschäftsupdates und anschließendes asynchrones Veröffentlichen. Debezium sagt, das Outbox-Muster vermeidet Inkonsistenzen zwischen dem internen Zustand eines Dienstes und den von anderen Diensten konsumierten Events. NServiceBus geht noch weiter und zeigt, wie Outbox-Verarbeitung eingehende Nachrichten entdoppliert und Zombie-Datensätze sowie Geister-Nachrichten vermeidet.
Dies ist die Architektur, die ich für Dienste empfehle, die Daten besitzen und Integrations-Events veröffentlichen:
- Validieren und persistieren Sie den Befehl unter einem Idempotenzschlüssel.
- Schreiben Sie Geschäftszustand und Outbox-Event in einer lokalen Transaktion.
- Lassen Sie CDC oder einen Outbox-Dispatcher das Event veröffentlichen.
- Machen Sie Downstream-Consumer ebenfalls idempotent.
Outbox entfernt nicht die Notwendigkeit für idempotente Consumer. Es entfernt die Notwendigkeit, so zu tun, als ob ein Datenbank-Commit und ein Broker-Publish eine magische verteilte Transaktion sein könnten, wenn sie das normalerweise nicht können.
Webhooks sind nur Nachrichten mit besserem Branding
Behandeln Sie eingehende Webhooks genau wie Nachrichten von einer ungesicherten Netzwerkkante.
GitHub dokumentiert, dass Zustellungen in falscher Reihenfolge eintreffen können, empfiehlt die Verwendung von X-Hub-Signature-256 zur Verifizierung der Authentizität und stellt X-GitHub-Delivery als einzigartigen Zustellungs-Identifikator bereit. Es wird auch darauf hingewiesen, dass Neuzustellungen dieselbe Zustellungs-ID wiederverwenden.
Also ist die Architektur straightforward (einfach):
- Verifizieren Sie zuerst die Signatur
- Verwenden Sie die Delivery-GUID als Entduplicierungsschlüssel
- Persistieren Sie den Empfang vor Seiteneffekten
- Machen Sie Handler bewusst für die Reihenfolge, anstatt Ankunftsreihenfolge anzunehmen
- Enqueuen Sie die schwere Arbeit und geben Sie schnell zurück
Wenn Ihr Webhook-Handler direkt in Geschäftstabellen schreibt, bevor er den Empfang aufzeichnet, ist er nicht produktionsreif. Er macht nur schneller doppelte Fehler.
Sagas und Workflow-Engines benötigen immer noch Idempotenz
Sagas und durable Workflow-Engines löschen das Problem nicht. Sie machen es sichtbar.
Temporal empfiehlt, Activities idempotent zu schreiben, weil Activities nach Fehlern oder Timeouts neu versucht werden können. Seine Dokumente nennen sogar den Sonderfall, in dem ein Worker einen externen Seiteneffekt erfolgreich abschließt, aber vor der Meldung der Fertigstellung abstürzt, wodurch die Activity erneut ausgeführt wird. Temporal schlägt auch vor, eine Kombination aus Workflow Run ID und Activity ID als stabilen Idempotenzschlüssel zu verwenden, wenn man Downstream-Dienste aufruft. Wenn Sie dies in der Service-Orchestrierung anwenden, deckt Go Microservices für AI/ML Orchestrierung die breiteren Workflow-Trade-offs ab.
Das ist genau das richtige mentale Modell. Eine Workflow-Engine kann Ausführungsverlauf bewahren und Neuversuche koordinieren. Sie kann nicht rückwirkend eine Karte entlasten oder eine E-Mail ungesendet lassen, es sei denn, Ihre Anwendung gibt ihr idempotente Schritte und idempotente Kompensationen.
Das Gleiche gilt für Sagas. Temporals eigene Saga-Leitfaden beschreibt kompensierende Aktionen, die ausgeführt werden, wenn ein Schritt fehlschlägt. Diese Kompensationen müssen ebenfalls idempotent sein. Wenn „Zahlung erstatten“ zweimal ausgeführt wird, haben Sie möglicherweise den ursprünglichen Fehler gelöst, indem Sie einen neuen geschaffen haben.
Meine Regel hier ist brutal und einfach. Jede Activity, jeder Command-Handler und jede Kompensation, die die Außenwelt berührt, sollte entweder natürlich idempotent sein oder einen echten Idempotenzschlüssel an das Downstream-System weitergeben.
Wie man Idempotenz vor der Produktion testet
Die meisten Teams testen glückliche Pfade und sind dann überrascht, wenn Neuversuche erfolgen. Das reicht nicht aus.
Sie sollten automatisierte Tests für mindestens diese Fälle haben:
- Der Server committed die Mutation, aber die Antwort erreicht nie den Client
- Zwei identische Anfragen rassen mit demselben Idempotenzschlüssel
- Derselbe Schlüssel wird mit einer anderen Payload wiederverwendet
- Ein Consumer committed seine Datenbankarbeit und stürzt vor dem Ack ab
- Ein Webhook wird mit derselben Delivery-ID wiedergegeben
- Ein Outbox-Dispatcher veröffentlicht dasselbe Event mehr als einmal
- Eine Workflow-Activity schließt den externen Aufruf ab und stürzt vor der Meldung der Fertigstellung ab
- Ein Idempotenzdatensatz läuft ab und ein echter verspäteter Neuversuch trifft ein
AWS empfiehlt explizit umfassende Testsuiten, die erfolgreiche Anfragen, fehlgeschlagene Anfragen und doppelte Anfragen einschließen. Dieser Rat ist prosaisch und absolut korrekt.
Ich würde noch einen weiteren Fehler-Drill hinzufügen. Verifizieren Sie, dass die wiedergegebene Antwort semantisch äquivalent zum ersten Ergebnis ist. AWS diskutiert spät ankommende Neuversuche und argumentiert für Antworten, die die ursprüngliche Bedeutung bewahren, auch nachdem sich der zugrunde liegende Zustand geändert hat. Das ist der Unterschied zwischen „kein zusätzlicher Seiteneffekt ist aufgetreten“ und „der Aufrufer hat immer noch einen konsistenten Vertrag“.
Gefällige Regeln, die echte Systeme retten
Hier sind die Regeln, die ich in einer Architektur-Review durchsetzen würde.
Erstens: Idempotenzschlüssel gehören zum Geschäftsentent, nicht zu Transportversuchen.
Zweitens: Grenzen Sie jeden Schlüssel nach Tenant und Operation ein. Globale Schlüsselräume sind der Ort, an dem unverwandte Anfragen kollidieren.
Drittens: Persistieren Sie die Entduplicierungsentscheidung atomar mit der Mutation. Wenn das nicht zutrifft, ist das Design falsch.
Viertens: Verwerfen Sie Neuversuche mit gleichem Schlüssel, aber unterschiedlicher Payload. Stripe und AWS tun dies aus gutem Grund.
Fünftens: Behalten Sie Schlüssel für den vollständigen Replay-Horizont des Geschäftsprozesses, nicht für das kürzeste Queue-Fenster.
Sechstens: Kombinieren Sie Produzenten mit einer Outbox und Consumer mit Nachrichten-ID-Tracking. Eine Seite ohne die andere ist ein halbes Design.
Siebtens: Propagieren Sie dieselbe Operationsidentität downstream, wenn die Geschäftshandlung dieselbe ist. AWS empfiehlt explizit, das Idempotenz-Token entlang der Verarbeitungskette weiterzugeben.
Achtens: Gehen Sie niemals davon aus, dass genau-einmal-Marketing die Notwendigkeit für idempotente Seiteneffekte entfernt.
Wenn das streng klingt, gut. Idempotenz ist der Ort, an dem optimistische Architektur auf Produktionsrealität trifft. Sie benötigen nicht überall Komplexität. Aber überall dort, wo doppelte Seiteneffekte Geld, Zustand oder Vertrauen schädigen würden, sollte Idempotenz ein First-Class-Teil des Vertrags sein.