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.
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.
| Ansatz | Garantie | Kosten | Wann wählen |
|---|---|---|---|
| Eindeutigkeitsbedingung in der Datenbank | Atomar, hält Nebenläufigkeit stand | Ein Index, eine Tabelle | Allgemeinfall, transaktionale Schreibvorgänge |
Anwendungssperre (Redis SET NX) | Schnell, außerhalb der Datenbank | Externe Abhängigkeit, Ablauf zu verwalten | Hoher Verkehr, Antwort anderswo gespeichert |
| Deduplizierung per Event-Kennung | Einfach auf Konsumentenseite | Deckt nur Ströme mit stabiler Kennung ab | Webhooks, 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.
