Zum Inhalt springen
Zapolu

17. Juni 2026, Luboš Zápotočný

Webhooks kommen doppelt. Rechnen Sie damit.

Payment- und Plattform-Webhooks kommen im Retry-Fenster mindestens einmal an, nicht genau einmal. Welche Probleme doppelte Zustellung verursacht und wie Idempotenz sie abfängt.

Lesen Sie die Webhook-Dokumentation eines beliebigen ernstzunehmenden Anbieters, und Sie finden in der einen oder anderen Form dieselbe Warnung. Stripe empfiehlt, die IDs der verarbeiteten Events zu protokollieren, weil ein Endpoint dasselbe Event mehr als einmal erhalten kann. Adyen weist darauf hin, dass dasselbe Event zweimal eintreffen kann und Duplikate behandelt werden müssen. Shopify stellt einen Header X-Shopify-Webhook-Id zum Deduplizieren bereit und schreibt dazu, dass die Zustellung überhaupt nicht garantiert ist. Alle drei geprüft am 13. Juli 2026: Stripe, Adyen, Shopify.

Innerhalb des Retry-Fensters lautet der Vertrag at-least-once, nicht exactly-once. Bleibt Ihr Endpoint das ganze Fenster über nicht erreichbar, kommt das Event gar nicht an, weshalb ein Abgleich-Job, der den Zustand über die API des Anbieters nachlädt, das Sicherheitsnetz unter der gesamten Integration ist. Exactly-once-Zustellung über ein unzuverlässiges Netz ist nicht erreichbar, also wählen die Anbieter die sichere Variante: Sind sie nicht sicher, dass Sie ein Event erhalten haben, schicken sie es noch einmal.

Die meisten Integrationen sind geschrieben, als gäbe es diese Klausel nicht. Sie funktionieren in der Demo und im ersten Monat; dann passiert irgendwo zwischen dem Anbieter und der 200er-Antwort ein Timeout, und dasselbe order.paid-Event kommt zweimal an.

Was doppelte Zustellung konkret verursacht

  • Der Kunde erhält zwei Bestellbestätigungen.
  • Der Bestand wird zweimal abgebucht, und Artikel stehen falsch als „ausverkauft“.
  • Eine Rechnung wird zweimal gestellt, und die Buchhaltung findet sie Monate später.
  • Ein Treueguthaben wird zweimal gutgeschrieben, was niemandem auffällt, bis die Zahlen nicht mehr aufgehen.

Der Anbieter hat getan, was er dokumentiert hat. Der Handler hat sich auf eine Garantie verlassen, die der Anbieter nie gegeben hat.

Idempotenz ist die Lösung

Ein idempotenter Handler erzeugt denselben Endzustand, egal ob ein Event einmal verarbeitet wird oder fünfmal. Diese eine Eigenschaft absorbiert Retries, manuelle Replays und eine Klasse von Race Conditions. Sie herzustellen ist vor allem eine Frage der Disziplin:

  1. Geben Sie jedem Event einen Schlüssel. Anbieter schicken eine Event-ID; tut Ihrer das nicht, leiten Sie eine aus stabilen Feldern ab. Speichern Sie verarbeitete IDs und überspringen Sie Duplikate. Setzen Sie das mit einem Unique Constraint in der Datenbank durch statt im Anwendungscode: Die Prüfung im Anwendungscode hat ein Race-Fenster, der Constraint nicht.
  2. Ziehen Sie absoluten Zustand relativen Änderungen vor. „Status auf bezahlt setzen“ ist von Natur aus idempotent. „Bestand um 2 verringern“ ist es nicht. Schickt der Anbieter Deltas, übersetzen Sie sie in Upserts auf Ihren eigenen Datensatz zum Event statt in unbedingte Arithmetik.
  3. Rechnen Sie auch mit falscher Reihenfolge. Retries kommen außerdem nicht in der Reihenfolge an, in der die Events entstanden sind: order.updated kann vor order.created landen. Versionsnummern oder Timestamps am Zieldatensatz („nur anwenden, wenn neuer“) lösen das, was Annahmen über die Reihenfolge nicht lösen.

Der Rest der Checkliste

Idempotenz ist der Kern. Drei Praktiken darum herum decken den Rest ab:

  • Schnell bestätigen, asynchron verarbeiten. Signatur prüfen, Event persistieren, 200 zurückgeben und die eigentliche Arbeit aus einer Queue heraus erledigen. Langsame synchrone Handler verursachen die Timeouts, die genau die Retries verursachen, um die Sie sich Sorgen gemacht haben.
  • Authentifizieren Sie jeden Webhook. Ein Webhook-Endpoint ist eine unauthentifizierte URL im öffentlichen Internet, die Ihren Geschäftszustand verändert. Die meisten Anbieter signieren die Payload mit einem HMAC, den Sie nachrechnen und vergleichen. Manche schicken gar keine Payload: Eine GoPay-Benachrichtigung ist ein HTTP-GET mit der Payment-ID, und der maßgebliche Zustand muss über die GoPay-API zurückgelesen werden, bevor Sie danach handeln (GoPay, geprüft am 13. Juli 2026). So oder so: Handeln Sie nie auf Basis des Requests, wie er hereinkommt.
  • Halten Sie eine Dead-Letter-Queue mit Replay-Tool. Manche Events scheitern aus Gründen, die kein Retry behebt: eine gelöschte SKU, eine Lücke im Mapping. Legen Sie sie dort ab, lassen Sie sich benachrichtigen, und machen Sie das Replay eines einzelnen Events ohne SSH-Zugriff auf die Produktion möglich. Fällt ein Anbieter für Stunden aus, holen Sie die Events per Replay nach, ohne einen Incident zu melden.

Ein Test deckt all das ab: Nehmen Sie die Events von gestern aus dem Staging und stellen Sie jedes davon zweimal zu, in gemischter Reihenfolge. Stimmt der Endzustand mit der Produktion überein, sind Ihre Handler wirklich idempotent.

Das ist der Standard, nach dem wir Integrationen bauen. Es ist der Großteil dessen, was unsere Integrationsseite beschreibt, und dasselbe Denken steckt in unserer individuellen Backend-Entwicklung. Und wenn Ihre Bestandszahlen bereits auseinanderlaufen, gehört die Webhook-Schicht zu den ersten Stellen, an denen man nachsieht.