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 ernsthaften Anbieters (Stripe, Shopify, Adyen, GoPay), und Sie finden in der einen oder anderen Form dieselbe Warnung: Zustellungen können mehrfach erfolgen. 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 null Mal an, weshalb ein Abgleich-Job, der den Zustand über die API des Anbieters nachlädt, die Absicherung unter allem anderen bildet. Exactly-once-Zustellung über ein unzuverlässiges Netz ist bekanntermaßen unmöglich, also tun die Anbieter das Vernünftige: 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 bekommt zwei Bestellbestätigungen.
  • Der Bestand wird zweimal abgebucht; so entsteht das Phantom-„ausverkauft“.
  • Eine Rechnung wird zweimal gestellt, was spätestens der Buchhaltung auffällt.
  • Ein Treueguthaben wird zweimal gutgeschrieben, was niemandem auffällt, bis die Zahlen nicht mehr aufgehen.

Nichts davon ist ein Anbieter-Bug. Der Anbieter hat getan, was er dokumentiert hat. Der Handler hat sich auf ein Versprechen verlassen, das niemand gegeben hat.

Idempotenz ist die eigentliche 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 ganze Klasse von Race Conditions gleich mit. Sie zu bekommen ist vor allem 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 „habe ich das schon gesehen?“ hat dort 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 gegen Ihre eigene Aufzeichnung des Events statt in blinde Arithmetik.
  3. Rechnen Sie auch mit falscher Reihenfolge. Retries kommen außerdem nicht unbedingt 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“) fangen ab, was Annahmen über die Reihenfolge nicht können.

Der Rest der Checkliste

Idempotenz ist der Kern; drei ergänzende Praktiken unterscheiden Integrationen, die im Produktivbetrieb stabil laufen, von solchen, die Support-Tickets erzeugen:

  • 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 laufen in Timeouts, und jeder Timeout löst genau die Retries aus, die Sie vermeiden wollten.
  • 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 (GoPay) geben Ihnen nur eine ID und erwarten, dass Sie den maßgeblichen Zustand über ihre API neu abrufen. So oder so: Handeln Sie nie auf Basis des Requests, wie er hereinkommt.
  • Halten Sie eine Dead-Letter-Queue mit Replay-Werkzeug. Manche Events scheitern aus Gründen, die kein Retry behebt: eine gelöschte SKU, eine Lücke im Mapping. Parken Sie sie, alarmieren Sie darauf, und machen Sie das Replay eines einzelnen Events zu einem Button statt zu einer SSH-Session. Fällt die Drittanbieter-API ein paar Stunden aus, erspart Ihnen das Replay-Werkzeug den Incident.

Der Test für all das ist erfreulich einfach: 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, ist Ihre Integration gegen doppelte Zustellung tatsächlich robust.

Das ist der Standard, nach dem wir Integrationen bauen. Es ist der Großteil dessen, was „gebaut, um weiterzulaufen, wenn die API des Drittanbieters gerade klemmt“ auf unserer Services-Seite bedeutet, und dasselbe Denken steckt in unserer individuellen Backend-Entwicklung. Und wenn Ihre Bestandszahlen bereits driften, gehört die Webhook-Schicht zu den üblichen Verdächtigen.