Zum Inhalt springen

Das Magazin für Web-Handwerker Sonntag, 30. August 2026

Backend

Idempotenz-Schlüssel: APIs und Webhooks gefahrlos wiederholen

Derselbe Netzwerkaufruf kann zweimal eintreffen: ein Verbindungsabbruch, ein automatischer Wiederholungsversuch, ein erneut zugestellter Webhook. Ein Idempotenz-Schlüssel lässt den Server die Wiederholung erkennen und das erste Ergebnis zurückgeben, ohne Dublette und ohne doppelte Belastung.

Idempotence : requetes rejouables

Derselbe Netzwerkaufruf kann zweimal eintreffen: die Verbindung bricht während der Antwort ab, der Client wiederholt von sich aus, ein Anbieter stellt seinen Webhook erneut zu. Ohne Schutzvorkehrung legt der Server zwei Bestellungen an, verschickt zwei E-Mails oder belastet zweimal. Der Idempotenz-Schlüssel ist der Standardmechanismus, der die Wiederholung erkennt und das Ergebnis des ersten Aufrufs unverändert zurückgibt.

Warum derselbe Aufruf zweimal eintrifft

Die HTTP-Semantik (RFC 9110) kennzeichnet GET, PUT und DELETE als idempotent: sie erneut zu senden lässt den Server im selben Zustand. POST ist es nicht, und genau dieses Verb legt Ressourcen an, löst Zahlungen aus oder verschickt Nachrichten. Ein sauber gebauter Client wiederholt nach einer überschrittenen Wartezeit, ohne zu wissen, ob die erste Anfrage vor dem Abbruch verarbeitet wurde.

Dieselbe Ungewissheit bestimmt Nachrichtenschlangen und Webhooks, die eine Zustellung mindestens einmal garantieren. Ein Konsument erhält daher denselben Event mitunter zweimal, und ein Doppelklick auf einen Bestellknopf erzeugt dieselbe Kollision im Browser. Das Gemeinsame dieser Fälle: der Absender weiß nicht, ob die Wirkung bereits eingetreten ist.

Was ein Idempotenz-Schlüssel garantiert und was nicht

Das Prinzip passt in einen Satz: der Client hängt jeder fachlichen Operation eine eindeutige Kennung an, übermittelt im Header Idempotency-Key. Der Server verknüpft diesen Schlüssel mit dem erzeugten Ergebnis. Taucht der Schlüssel erneut auf, gibt er die bereits berechnete Antwort zurück, statt die Arbeit noch einmal auszuführen.

POST /v1/payments HTTP/1.1
Host: api.exemple.com
Idempotency-Key: 5f0c8b1e-2a3d-4c6f-9b21-7d0e4a9c1f88
Content-Type: application/json

{"amount": 4200, "currency": "eur", "order": "cmd_9182"}

Die Reichweite des Mechanismus verdient eine klare Ansage. Ein Idempotenz-Schlüssel dedupliziert zwei Anfragen mit demselben Schlüssel; er macht nichts rückgängig und gleicht nicht zwei verschiedene Schlüssel mit derselben Absicht ab. Er verwandelt ein netzseitiges „mindestens einmal“ in ein „genau einmal“ auf Ebene der Wirkung, sofern der Schlüssel die fachliche Absicht benennt und nicht den HTTP-Versuch.

Ein Idempotenz-Schlüssel macht die Operation nicht umkehrbar: er verhindert nur, dass sie zweimal geschieht.

Der Lebenszyklus einer idempotenten Anfrage

Beim Eintreffen sucht der Server den Schlüssel. Fehlt er, wird er als „in Bearbeitung“ vermerkt, die Arbeit ausgeführt und danach Antwortcode und Rumpf gespeichert. Vorhanden und abgeschlossen, gibt der Server die gespeicherte Antwort zurück. Vorhanden, aber noch laufend, meldet er, dass eine identische Anfrage bereits unterwegs ist.

Client Idempotency-Key Server unbekannter Schlüssel bekannter Schlüssel Ausführen und speichern Gespeichertes zurückgeben

Schlüssel speichern: der Kern des Mechanismus

Alles ruht auf einer eigenen Tabelle und einer Eindeutigkeitsbedingung. Diese Bedingung, nicht eine vorgelagerte Prüfung in der Anwendung, entscheidet zwischen zwei nebenläufigen Anfragen: die gewinnende Einfügung führt die Arbeit aus, die andere schlägt fehl und wechselt zum Lesen des gespeicherten Ergebnisses.

CREATE TABLE idempotency_keys (
    id            BIGSERIAL PRIMARY KEY,
    scope         TEXT        NOT NULL,        -- compte + endpoint
    idem_key      TEXT        NOT NULL,
    request_hash  TEXT        NOT NULL,        -- empreinte du corps
    status        TEXT        NOT NULL,        -- 'in_progress' | 'completed'
    response_code SMALLINT,
    response_body JSONB,
    created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
    expires_at    TIMESTAMPTZ NOT NULL,
    UNIQUE (scope, idem_key)                   -- verrou atomique
);

Der Handler ist um diese atomare Einfügung herum gebaut. Er vergleicht zudem einen Fingerabdruck des Rumpfes: derselbe Schlüssel mit einer anderen Nutzlast deutet auf missbräuchliche Wiederverwendung hin, besser abgewiesen als mit einem irreführenden Ergebnis bedient.

public function store(Request $request): JsonResponse
{
    $key   = $request->header('Idempotency-Key');
    $scope = $request->user()->id . ':POST /v1/payments';
    $hash  = hash('sha256', $request->getContent());

    // Insertion atomique : la contrainte d'unicité arbitre les concurrents
    try {
        DB::table('idempotency_keys')->insert([
            'scope' => $scope, 'idem_key' => $key, 'request_hash' => $hash,
            'status' => 'in_progress', 'expires_at' => now()->addDay(),
        ]);
    } catch (QueryException $e) {                 // Cle deja presente : rejeu ou requete concurrente
        $row = DB::table('idempotency_keys')
            ->where(compact('scope'))->where('idem_key', $key)->first();

        if ($row->request_hash !== $hash) {
            return response()->json(['error' => 'key_reuse'], 422); // Meme cle, corps different : reutilisation interdite
        }
        if ($row->status === 'in_progress') {
            return response()->json(['error' => 'in_flight'], 409); // Operation encore en cours cote serveur
        }
        return response()->json($row->response_body, $row->response_code); // Rejeu : on renvoie la reponse d'origine, a l'identique
    }

    $payment = $this->charge($request);           // Le traitement metier n'a lieu qu'une fois

    DB::table('idempotency_keys')->where(compact('scope'))->where('idem_key', $key)
        ->update(['status' => 'completed', 'response_code' => 201,
                  'response_body' => $payment]);
    return response()->json($payment, 201);
}

Die ursprüngliche Antwort bleibt unverändert erhalten, Statuscode inbegriffen, um bei der Wiederholung erneut ausgeliefert zu werden.

{
  "scope": "acct_42:POST /v1/payments",
  "idem_key": "5f0c8b1e-2a3d-4c6f-9b21-7d0e4a9c1f88",
  "request_hash": "b9f3...c1",
  "status": "completed",
  "response_code": 201,
  "response_body": { "id": "pay_7Kd2", "amount": 4200, "status": "succeeded" },
  "expires_at": "2026-08-30T22:00:00Z"
}

Je nach Kontext bestehen mehrere Deduplizierungsstrategien nebeneinander.

AnsatzGarantieKostenWann wählen
Eindeutigkeitsbedingung in der DatenbankAtomar, hält Nebenläufigkeit standEin Index, eine TabelleAllgemeinfall, transaktionale Schreibvorgänge
Anwendungssperre (Redis SET NX)Schnell, außerhalb der DatenbankExterne Abhängigkeit, Ablauf zu verwaltenHoher Verkehr, Antwort anderswo gespeichert
Deduplizierung per Event-KennungEinfach auf KonsumentenseiteDeckt nur Ströme mit stabiler Kennung abWebhooks, Nachrichtenschlangen

Die Fallen im Produktivbetrieb

Nebenläufigkeit. Zwei Anfragen mit demselben Schlüssel treffen gleichzeitig ein. Ohne atomare Einfügung bestehen beide die Prüfung „gibt es den Schlüssel?“ und führen die Arbeit aus. Die Eindeutigkeitsbedingung löst den Konflikt beim Schreiben: der Verlierer erhält einen 409 oder wartet auf die laufende Antwort, nie eine zweite Belastung.

Drei weitere Fallstricke kehren regelmäßig wieder. Der Geltungsbereich des Schlüssels muss Konto und Endpunkt umfassen: ein zwischen zwei Clients geteilter Schlüssel öffnet ein Antwortleck. Der Ablauf verhindert eine endlos wachsende Tabelle; ein Fenster von vierundzwanzig Stunden bis zu einigen Tagen deckt realistische Wiederholungsfenster ab. Schließlich ersetzt Idempotenz nicht die transaktionale Konsistenz: der fachliche Schreibvorgang und die Aktualisierung des Schlüsselstatus sollten dieselbe Transaktion teilen, sonst überlebt eine als „abgeschlossen“ markierte Antwort eine tatsächlich fehlgeschlagene Belastung.

Client-Seite und Anbieter-Seite: Schlüssel erzeugen und konsumieren

Der Client erzeugt einen zufälligen Schlüssel, etwa ein UUID v4, einmal je fachlicher Absicht. Die entscheidende Gewohnheit: diesen Schlüssel für alle Wiederholungen derselben Operation behalten und ihn vor allem nicht bei jedem HTTP-Versuch neu erzeugen, sonst verschwindet die Garantie. Die großen Zahlungs-APIs bieten den Header unter genau diesem Namen an und geben bei einer Kollision einen ausdrücklichen Fehler zurück.

Auf der Seite des Event-Konsums ist die Logik symmetrisch. Ein Webhook trägt eine stabile Kennung; der Konsument behält die bereits verarbeiteten Kennungen und ignoriert erneute Zustellungen. Es ist dieselbe Idempotenz, angewandt auf den Eingang statt auf den Ausgang.

Was bleibt

  • Ein Idempotenz-Schlüssel verwandelt eine „mindestens einmal“-Zustellung in eine „genau einmal“-Wirkung, sofern er die fachliche Absicht benennt.
  • Zuverlässige Deduplizierung ruht auf einer Eindeutigkeitsbedingung in der Datenbank, nicht auf einer vorgelagerten Prüfung in der Anwendung.
  • Der Schlüssel wird je Konto und Endpunkt gefasst, läuft nach einem kurzen Fenster ab, und die ursprüngliche Antwort wird unverändert erneut ausgeliefert.
  • Auf dem Client ein Schlüssel je Operation, bei jeder Wiederholung wiederverwendet; auf dem Konsumenten Deduplizierung per Event-Kennung.

Ich habe mehr Vorfälle von Doppelbelastung durch einen schlecht gefassten Schlüssel gesehen als durch einen fehlenden: ein Schlüssel, der für zwei verschiedene Operationen wiederverwendet oder bei jedem Versuch neu erzeugt wird, und die Garantie bricht lautlos zusammen. Die Regel, die ich aus der Praxis behalte, lässt sich auf zwei Punkte bringen: ein Schlüssel je fachlicher Absicht, nie je HTTP-Anfrage, und ein Test, der den Aufruf vor jedem Produktivgang bewusst wiederholt. — Simon Janvier

Zum Weiterlesen

Referenzspezifikation: The Idempotency-Key HTTP Header Field, IETF-Arbeitsgruppe HTTP API.

Ebenfalls auf Mail Studio

Teilen LinkedIn Bluesky Hacker News E-mail

Ebenfalls lesenswert