---
title: "Idempotenz in der Praxis: Warum Ihr Retry gerade doppelt abgebucht hat"
description: "Idempotenz ist kein Header, den man hinzufügt. Sie ist eine Eigenschaft, die Ihre Speicherschicht hat oder eben nicht. Was RFC 9110 wirklich sagt, wie das Stripe-Modell funktioniert und welche Race Condition die meisten Implementierungen zerlegt."
author: Aleksei Aleinikov
date: 2026-08-14
lang: de
tags: [idempotenz, idempotency key, api retries, exactly once, verteilte systeme]
canonical: https://www.alekseialeinikov.com/de/blog/topics/architecture/idempotenz-in-der-praxis-api-retries-2026
source: alekseialeinikov.com
---

# Idempotenz in der Praxis: Warum Ihr Retry gerade doppelt abgebucht hat

Der Retry ist nicht der Fehler. Der Retry war korrektes Verhalten — Ihre Client-Bibliothek hat exakt das getan, wofür sie konfiguriert war, als die Verbindung abriss.

Der Fehler ist, dass Ihr Server keine Möglichkeit hatte zu erkennen, dass er diesen Request schon einmal gesehen hatte.

Das ist der folgenschwerste Punkt im API-Design, den fast niemand vollständig richtig macht — und er hat einen Namen, den jeder kennt und den erstaunlich wenige präzise erklären können: **Idempotenz**.

![Idempotenz ist kein Header, den man hinzufügt — sie ist eine Eigenschaft der Speicherschicht.](https://www.alekseialeinikov.com/blog/idempotency-2026.webp)

## Die Definition, auf die es ankommt

Eine Operation ist idempotent, wenn ihre mehrfache Ausführung denselben Effekt hat wie eine einmalige.

**RFC 9110** — der aktuelle Standard für HTTP-Semantik — formuliert das bewusst über die *Absicht*:

> *„Eine Request-Methode gilt als ‚idempotent‘, wenn der beabsichtigte Effekt mehrerer identischer Requests mit dieser Methode auf den Server derselbe ist wie der Effekt eines einzelnen solchen Requests."*

Das Wort **beabsichtigt** leistet hier viel Arbeit. Die Spezifikation stellt direkt danach klar, dass ein Server jeden Request einzeln protokollieren, eine Versionshistorie führen oder andere nicht-idempotente Nebenwirkungen haben darf. Idempotenz betrifft das, was der Client angefordert hat — nicht die Frage, ob sich irgendwo überhaupt etwas verändert hat.

Welche Methoden erfüllen das?

| Methode | Sicher | Idempotent |
|---|---|---|
| GET, HEAD | ja | ja |
| OPTIONS, TRACE | ja | ja |
| PUT | nein | **ja** |
| DELETE | nein | **ja** |
| **POST** | nein | **nein** |
| PATCH | nein | nein |
| CONNECT | nein | nein |

Sicher und idempotent sind unterschiedliche Eigenschaften. Jede sichere Methode ist idempotent; umgekehrt gilt das nicht. `DELETE` ist idempotent und ganz sicher nicht sicher.

Ein Vorbehalt zu dieser Tabelle: `PATCH` wird von RFC 9110 gar nicht definiert. Die Methode stammt aus **RFC 5789**, wo unmissverständlich steht, dass „PATCH weder sicher noch idempotent" ist — ergänzt um ein Detail, das die These dieses Artikels vorwegnimmt: „Ein PATCH-Request kann so gestellt werden, dass er idempotent ist." Nicht die Methode entscheidet. Die Implementierung entscheidet.

Beachten Sie, was in der Ja-Spalte fehlt: **POST**. Diese eine Tatsache ist der Grund für diesen gesamten Artikel.

## Die Spezifikation sagt, man solle nicht wiederholen. Alle wiederholen trotzdem.

RFC 9110 sagt ausdrücklich, warum Idempotenz überhaupt definiert wurde:

> *„Idempotente Methoden zeichnen sich dadurch aus, dass der Request automatisch wiederholt werden kann, wenn ein Kommunikationsfehler auftritt, bevor der Client die Antwort des Servers lesen konnte."*

Und dann zieht sie die Grenze:

> *„Ein Client SOLLTE NICHT automatisch einen Request mit einer nicht-idempotenten Methode wiederholen, es sei denn, er verfügt über Mittel zu wissen, dass die Request-Semantik tatsächlich idempotent ist… Ein Proxy DARF NICHT automatisch nicht-idempotente Requests wiederholen."*

Und nun die Realität: Service Meshes wiederholen. Load Balancer wiederholen. HTTP-Client-Bibliotheken wiederholen standardmäßig. Mobile Apps wiederholen, weil der Nutzer zweimal getippt hat, da der Spinner hängen zu bleiben schien. Message Queues stellen erneut zu.

Der Rat der Spezifikation ist richtig und vollkommen undurchsetzbar. Die praktische Frage lautet deshalb nicht *„wie verhindere ich Retries?"*, sondern **„wie mache ich meinen POST-Endpunkt sicher wiederholbar?"**

Genau dafür gibt es den `Idempotency-Key`-Header.

## Idempotency-Key ist ein De-facto-Standard, kein Standard

Bevor Sie darauf aufbauen, sollten Sie das wissen: Es **gibt** einen IETF-Draft — `draft-ietf-httpapi-idempotency-key-header` in der httpapi-Arbeitsgruppe — und er ist **abgelaufen, ohne jemals ein RFC zu werden**.

Wenn Ihnen also jemand sagt, das sei „der Standard-Header", lautet die korrekte Fassung: Es ist eine Konvention, die Stripe populär gemacht hat, die die meisten Zahlungs- und API-Plattformen übernommen haben und hinter der kein normatives Dokument steht. Der Headername ist branchenweit einheitlich. Die *Semantik* ist das, wofür sich der jeweilige Anbieter entschieden hat.

Das bedeutet: Aufbewahrungsfristen, Verhalten bei abweichenden Parametern und Umgang mit Nebenläufigkeit unterscheiden sich zwischen Anbietern. Lesen Sie die Dokumentation dessen, was Sie anbinden — nehmen Sie nichts an.

## Wie das Stripe-Modell tatsächlich funktioniert

Stripes Implementierung kommt einer Referenz am nächsten, und ihre Details sind durchdachter als die meisten Kopien davon.

```bash
curl https://api.stripe.com/v1/customers \
  -H "Idempotency-Key: 8f14e45f-ea8d-4b1c-9b3d-2c1a7e5f0a91" \
  -d description="Mein erster Kunde"
```

Die Regeln, präzise:

**Gespeichert wird das Ergebnis, nicht der Request.** Stripe sichert Statuscode und Body des ersten Requests zu einem Key — **unabhängig davon, ob er erfolgreich war oder fehlschlug**. Eine Wiederholung liefert dasselbe Ergebnis zurück, *einschließlich eines 500ers*. Das überrascht viele und ist genau richtig: Wenn der erste Versuch tatsächlich fehlschlug, ist es ehrlich, den Fehler erneut zu liefern. Hinter demselben Key heimlich einen neuen Versuch zu starten, wäre es nicht.

**Der Client erzeugt den Key.** Eine UUIDv4 oder ein ähnlicher Zufallsstring mit hoher Entropie, bis zu 255 Zeichen. Würde der *Server* ihn erzeugen, käme ein Retry mit einem neuen Key an — der ganze Mechanismus wäre sinnlos.

**Keine sensiblen Daten in Keys.** Keine E-Mail-Adressen, keine personenbezogenen Identifikatoren. Keys landen in Logs, Metriken und Fehlerberichten.

**Keys laufen ab.** Entfernt nach mindestens 24 Stunden; die Wiederverwendung eines gelöschten Keys erzeugt einen echten neuen Request.

**Parameter werden verglichen.** Kommt derselbe Key mit anderen Parametern an, ist das ein Fehler — es bedeutet, dass der Client sich vertan hat, nicht dass er wiederholt hat.

**Doch die Wiedergabe hat eine Grenze.** Alles, was *vor* dem Start des Endpunkts abgewiesen wird, fällt nicht darunter. Rate Limiting ist der klarste Fall: Stripes eigene Doku hält fest, dass ein `429` unter demselben Key ein anderes Ergebnis liefern kann, weil der Rate Limiter vor der Idempotenzschicht läuft. Dasselbe gilt für ein `401` ohne API-Key. Die Empfehlung für `4xx` insgesamt ist deutlich: lieber einen neuen Key erzeugen, als das alte Ergebnis für verbindlich zu halten.

**Und die feine Regel:** Ergebnisse werden erst gespeichert, *nachdem die Ausführung des Endpunkts begonnen hat*. Scheitert der Request an der Validierung oder **kollidiert er mit einem nebenläufig laufenden Request**, wird kein idempotentes Ergebnis abgelegt — und der Client darf es erneut versuchen.

Ein kleines Implementierungsdetail lohnt die Übernahme: Stripe kennzeichnet eine wiedergegebene Antwort mit dem Header `Idempotent-Replayed: true`. Das kostet nichts und macht aus der Frage „Hat mein Retry überhaupt etwas bewirkt?" statt einer Debugging-Sitzung eine Header-Prüfung.

In dieser Nebenläufigkeitsregel steckt die eigentliche Ingenieursarbeit.

![Der Lebenszyklus eines Idempotency-Keys: abwesend, in Bearbeitung und abgeschlossen sind drei verschiedene Zustände.](https://www.alekseialeinikov.com/blog/idempotency-states-2026.webp)

## Die Race Condition, die die meisten Implementierungen zerlegt

Hier die naive Umsetzung — und irgendeine Variante davon läuft gerade produktiv in einem Unternehmen ganz in Ihrer Nähe:

```python
# KAPUTT — bitte nicht ausliefern
existing = db.get_idempotency_record(key)
if existing:
    return existing.response          # Wiedergabe
result = charge_the_customer(params)  # ← zwei Requests können hier gleichzeitig sein
db.save_idempotency_record(key, result)
return result
```

Das Fenster zwischen Abfrage und Speicherung ist klein. Null ist es nicht. Zwei Retries, die 40 Millisekunden auseinander eintreffen — völlig normal, wenn ein mobiler Client in ein Timeout läuft und sofort neu verbindet — finden beide nichts, laufen beide durch, und dem Kunden wird zweimal abgebucht.

**Die Wurzel des Fehlers ist das Denken in zwei Zuständen.** Ein Key ist nicht bloß *vorhanden* oder *abwesend*. Er hat drei Zustände:

| Zustand | Bedeutung | Korrekte Antwort |
|---|---|---|
| **Abwesend** | Nie gesehen | Beanspruchen, dann ausführen |
| **In Bearbeitung** | Ein anderer Request hält ihn, noch nicht fertig | `409 Conflict` — später erneut |
| **Abgeschlossen** | Fertig, Ergebnis gespeichert | Gespeichertes Ergebnis zurückgeben |

Die mittlere Zeile ist keine Erfindung. Stripes eigene Statuscode-Referenz führt `409 Conflict` als „der Request kollidiert mit einem anderen Request (möglicherweise wegen Verwendung desselben Idempotenz-Keys)".

Die Lösung besteht darin, das Beanspruchen des Keys atomar zu machen — und zwar **bevor** irgendeine Arbeit beginnt:

```sql
-- Atomar beanspruchen. Entweder Sie haben die Zeile eingefügt, oder jemand anderes besitzt sie.
INSERT INTO idempotency_keys (key, tenant_id, request_fingerprint, state, created_at)
VALUES ($1, $2, $3, 'in_flight', now())
ON CONFLICT (tenant_id, key) DO NOTHING
RETURNING id;
```

Null zurückgegebene Zeilen bedeuten, dass Sie das Rennen verloren haben. Lesen Sie die vorhandene Zeile: Ist sie `complete`, geben Sie die gespeicherte Antwort zurück; ist sie noch `in_flight`, antworten Sie mit `409` und lassen den Client wiederkommen.

Und dann — dieser Teil wird gerne übersprungen — **schreiben Sie das Ergebnis in derselben Transaktion wie die fachliche Änderung**:

```sql
BEGIN;
  INSERT INTO payments (...) VALUES (...);
  UPDATE idempotency_keys
     SET state = 'complete', status_code = 201, response_body = $1
   WHERE tenant_id = $2 AND key = $3;
COMMIT;
```

Werden Buchung und Key-Eintrag getrennt committet, haben Sie die Race Condition nur verschoben statt beseitigt. Ein Absturz zwischen beiden Schreibvorgängen bedeutet: Das Geld ist geflossen, während der Key noch `in_flight` sagt.

Zwei Details verhindern, dass daraus ein eigener Ausfall wird:

**Hängende in-flight-Zeilen aufräumen.** Ein Prozess, der mitten im Request stirbt, hinterlässt einen Key für immer im Zustand `in_flight` — und dieser Key weist jeden Retry ab, bis jemand es bemerkt. Geben Sie in-flight-Einträgen eine Lease mit Timeout.

**Einen Fingerabdruck des Requests speichern.** Hashen Sie die relevanten Parameter. Kommt derselbe Key mit einem anderen Fingerabdruck an, weisen Sie ihn ab — der Client hat einen Fehler, und die stille Wiedergabe einer fremden Antwort wäre schlimmer als eine Fehlermeldung.

![Derselbe Key trifft zweimal ein: Der erste beansprucht ihn atomar, der zweite wird vertröstet.](https://www.alekseialeinikov.com/blog/idempotency-race-2026.webp)

## Die Wahl des Keys

**Binden Sie ihn an den Aufrufer.** Immer. Ein Key allein ist ein globaler Namensraum — ein Mandant kann mit dem Key eines anderen kollidieren und, schlimmer noch, gezielt danach suchen. Die Unique-Constraint gehört auf `(tenant_id, key)`, niemals auf `key` allein.

**Lassen Sie den Client ihn erzeugen.** Vor dem ersten Versuch, unverändert über alle Retries hinweg. Das ist der gesamte Vertrag.

**Hashen Sie nicht den Request-Body zu einem Key.** Verlockend ist es: Payload hashen und als Key verwenden. Doch zwei berechtigt identische Requests — derselbe Kunde kauft innerhalb einer Minute zweimal denselben Kaffee — würden zu einem verschmelzen, und Sie verlören stillschweigend eine echte Bestellung. Content-Hashing erkennt *Duplikate*; ein Idempotency-Key identifiziert *eine beabsichtigte Operation*. Zwei verschiedene Aufgaben.

Den Key aus einem stabilen Objekt abzuleiten, mit dem der Nutzer ohnehin arbeitet — einer Warenkorb-ID, einer Bestellentwurf-ID — ist etwas anderes und völlig in Ordnung; Stripe schlägt genau das vor. Der Warenkorb identifiziert die Operation. Die Payload beschreibt sie nur.

**Legen Sie keine Bedeutung hinein.** Keine Kontonummern, keine E-Mail-Adressen, keine Zeitstempel, die Sie später parsen wollen.

## Das ist nicht nur ein HTTP-Problem

Dasselbe Prinzip gilt für alles, was zweimal zustellen kann — also für alles.

Message Broker sind der offensichtliche Fall: At-least-once-Zustellung ist die Norm, ein Consumer muss also auf dieselbe Nachricht zweimal vorbereitet sein. Der Mechanismus ist identisch — ein Deduplizierungsschlüssel, atomar zusammen mit der Änderung des Consumers gespeichert. Genau das habe ich durchgespielt, als es darum ging, [eine Million Benachrichtigungen zu versenden, ohne abzustürzen](https://www.alekseialeinikov.com/de/blog/topics/architecture/eine-million-benachrichtigungen-versenden-ohne-abzustuerzen-2026); die Dedupe-Tabelle dort ist eine Idempotenz-Tabelle mit anderem Hut.

Damit zum Begriff, der in verteilten Systemen die meiste Verwirrung stiftet.

**Exactly-once-Zustellung existiert nicht.** Über ein unzuverlässiges Netzwerk kann der Sender nicht unterscheiden, ob „der Request verloren ging" oder „die Antwort verloren ging". Er muss wählen: aufgeben (at-most-once) oder wiederholen (at-least-once). Eine dritte Option gibt es auf Transportebene nicht.

Beachten Sie das Wort *Zustellung*. Systeme, die mit Exactly-once-**Semantik** werben — Kafkas transaktionaler Producer ist das bekannteste Beispiel — brechen diese Regel nicht. Sie erreichen es genau so, wie dieser Artikel es beschreibt: mit Sequenznummern, Deduplizierung und atomaren Commits auf der Empfängerseite. Die Garantie ist echt. Sie wird gebaut, nicht transportiert.

Was Sie bauen *können*, ist **effectively-once**:

> At-least-once-Zustellung + idempotente Verarbeitung = der Effekt tritt genau einmal ein

Diese Umdeutung ist der praktische Gewinn. Hören Sie auf, Zustellung perfekt machen zu wollen. Machen Sie Wiederholung harmlos.

## Wie es richtig aussieht

- Jeder zustandsändernde `POST` und `PATCH` akzeptiert einen Idempotency-Key; `GET`, `PUT` und `DELETE` brauchen keinen
- Der Key wird atomar beansprucht, **bevor** Arbeit beginnt — nicht geprüft und dann gehandelt
- Ergebnis und fachliche Änderung werden in **derselben Transaktion** committet
- Keys sind an den authentifizierten Mandanten gebunden, mit Unique-Constraint auf dem Paar
- `in_flight`-Einträge haben eine Lease und werden aufgeräumt
- Ein Parameter-Fingerabdruck wird gespeichert, Abweichungen werden abgewiesen statt wiedergegeben
- Wiedergaben sind gekennzeichnet, damit ein Client ein gespeichertes Ergebnis von einem frischen unterscheiden kann
- Die Aufbewahrungsdauer ist dokumentiert, indiziert und wird tatsächlich durchgesetzt
- Jemand hat den nebenläufigen Fall getestet — zwei identische Requests gleichzeitig, nicht nacheinander

Der letzte Punkt ist der Unterschied zwischen einer Implementierung, die funktioniert, und einer, die nur so aussieht. Sequenzielle Tests laufen auch auf kaputtem Code grün. Schreiben Sie den Test, der beide Requests gleichzeitig abfeuert.

## Fazit

Idempotenz ist kein Header. Der Header ist lediglich die Art, wie sich zwei Systeme auf einen Namen für eine Operation einigen.

Die eigentliche Idempotenz lebt in Ihrer Speicherschicht: in einer Unique-Constraint, in einem atomaren Anspruch, in einer Transaktionsgrenze. Sind diese falsch, rettet Sie auch der korrekteste Umgang mit Headern nicht — und sind sie richtig, sind Sie selbst vor Clients sicher, die weit aggressiver wiederholen, als die Spezifikation erlaubt.

RFC 9110 sagt Clients, sie sollen nicht-idempotente Methoden nicht automatisch wiederholen. Sie tun es trotzdem. Bauen Sie für die Welt, die Sie haben.

Derselbe Instinkt zieht sich durch die [Entscheidung zwischen Monolith und Microservices](https://www.alekseialeinikov.com/de/blog/topics/architecture/monolith-vs-microservices-2026-ehrliche-abwaegung): Die Fehlerfälle, für die Sie entwerfen, sind die, die Sie nicht nachts wecken.
