bei POST, PUT, PATCH und DELETE
Netzwerkfehler passieren. Timeouts passieren. Was passiert, wenn ein Client eine Anfrage wiederholt, weil er keine Antwort bekommen hat? Bei idempotenten Methoden: nichts Schlimmes. Bei POST: möglicherweise eine Doppelbuchung, ein doppelter Datensatz oder eine zweifache Zahlung. Dieser Artikel erklärt, wie man REST-APIs so gestaltet, dass Retries sicher sind.
Inhaltsverzeichnis
- 1. Was Idempotenz genau bedeutet
- 2. GET, PUT, DELETE – warum idempotent?
- 3. Das POST-Problem: Doppelausführung und ihre Folgen
- 4. Idempotency-Keys: POST sicher machen
- 5. PATCH-Semantik: partiell und idempotent zugleich?
- 6. Retry-Strategien: Exponential Backoff und Jitter
- 7. Optimistic Locking mit ETag und If-Match
- 8. DELETE-Edge-Cases: 404 als Erfolg behandeln
- 9. HTTP-Methoden im Idempotenz-Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was Idempotenz genau bedeutet
Eine Operation ist idempotent, wenn sie beliebig oft ausgeführt werden kann, ohne dass sich das Ergebnis nach der ersten Ausführung ändert. Der Begriff stammt aus der Mathematik: Eine Funktion f ist idempotent, wenn f(f(x)) = f(x). Im HTTP-Kontext bedeutet das: Wenn derselbe Request n-mal gesendet wird, hat der Server nach dem ersten Request denselben Zustand wie nach dem n-ten Request. Die Antworten können sich unterscheiden (z.B. 200 beim ersten, 200 beim zweiten DELETE), aber der Serverzustand ist identisch.
Idempotenz ist kein Sicherheitsgarant – es geht nicht darum, ob eine Operation Daten verändert, sondern ob eine Wiederholung Schaden anrichtet. Ein PUT-Request, der einen Benutzer auf {"name": "Alice"} setzt, ist idempotent: zehnmal ausgeführt bleibt der Name Alice. Ein POST-Request, der eine Zahlung auslöst, ist nicht idempotent: zehnmal ausgeführt löst er zehn Zahlungen aus. Diese Eigenschaft ist für die Fehlerbehandlung in verteilten Systemen fundamental, weil Netzwerkfehler und Timeouts Retries erzwingen – und ein Retry einer nicht-idempotenten Operation verheerende Folgen haben kann.
HTTP unterscheidet explizit zwischen idempotenten und sicheren Methoden. Eine sichere Methode verändert keine Ressourcen (GET, HEAD, OPTIONS). Eine idempotente Methode darf Ressourcen verändern, aber eine Wiederholung darf keinen weiteren Effekt haben (PUT, DELETE, GET). POST ist weder sicher noch idempotent. PATCH ist per Spezifikation nicht zwingend idempotent, kann es aber sein – je nach Semantik des Patches.
2. GET, PUT, DELETE – warum idempotent?
GET ist idempotent und sicher: Es liest Daten, ändert sie nicht. Zehnmal dasselbe GET ausführen ändert nichts am Serverzustand. PUT ist idempotent, weil es eine Ressource auf einen vollständigen, definierten Zustand setzt. PUT /users/7 mit dem Body {"name": "Alice", "email": "alice@example.com"} ersetzt den Benutzer vollständig durch diesen Zustand. Ein zweites identisches PUT ändert nichts, weil der Zustand bereits identisch ist. Wichtig: PUT sendet immer die vollständige Ressource, nicht nur geänderte Felder.
DELETE ist idempotent im Sinne des Serverzustands: Nach dem ersten DELETE ist die Ressource weg. Ein zweites DELETE auf dieselbe URL findet nichts mehr vor – aber der Zustand (Ressource nicht vorhanden) bleibt derselbe. Die HTTP-Spezifikation erlaubt, dass das zweite DELETE entweder 204 (No Content) oder 404 (Not Found) zurückgibt. Für Retry-Logik ist es sinnvoll, 404 bei DELETE nicht als Fehler zu behandeln, sondern als Bestätigung, dass das Ziel bereits erreicht wurde.
HEAD und OPTIONS sind ebenfalls idempotent und sicher. CONNECT und TRACE sind in API-Kontexten irrelevant. Das Verständnis der Idempotenz-Eigenschaften jeder Methode ist die Grundlage für eine korrekte Retry-Strategie: Idempotente Methoden darf ein Client bei Timeout oder Netzwerkfehler sicher wiederholen. Nicht-idempotente Methoden müssen durch spezielle Mechanismen abgesichert werden.
3. Das POST-Problem: Doppelausführung und ihre Folgen
Das klassische Szenario: Ein Client schickt POST /orders mit einem Bestellbody. Das Netzwerk stirbt genau in dem Moment, in dem der Server die Bestellung gespeichert hat und die 201-Antwort absendet. Der Client bekommt keine Antwort, interpretiert das als Fehler und wiederholt den Request. Ergebnis: zwei identische Bestellungen in der Datenbank, aber der Kunde hat nur einmal bestellt. Oder schlimmer: der Request war eine Zahlung, und die Kreditkarte wurde zweimal belastet.
Das Problem ist fundamental: Der Client weiß bei einem Timeout nicht, ob der Server die Anfrage verarbeitet hat oder nicht. Bei GET ist das egal – ein Retry liest einfach nochmal. Bei POST kann der Retry katastrophale Folgen haben. Naïve Lösungen wie "zeige dem Client eine Fehlermeldung und lass ihn manuell nochmal klicken" sind kein echtes System-Design – sie verlagern das Problem auf den Nutzer und lösen es nicht in automatisierten Client-zu-API-Kommunikation.
# Unsicherer POST ohne Idempotency-Key — Doppelbuchung bei Retry möglich
curl -X POST https://api.mironsoft.de/orders \
-H "Content-Type: application/json" \
-d '{"productId": 15, "quantity": 2, "customerId": 7}'
# Timeout — Client weiß nicht, ob der Server die Bestellung gespeichert hat
# Retry desselben Requests => möglicherweise 2 Bestellungen in der DB
# Sicherer POST MIT Idempotency-Key — Server erkennt Duplikat
IDEM_KEY=$(uuidgen)
curl -X POST https://api.mironsoft.de/orders \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEM_KEY" \
-d '{"productId": 15, "quantity": 2, "customerId": 7}'
# Bei Timeout: exakt denselben Key wiederverwenden
curl -X POST https://api.mironsoft.de/orders \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEM_KEY" \
-d '{"productId": 15, "quantity": 2, "customerId": 7}'
# Server antwortet mit der gecachten Antwort der ersten Ausführung — kein Duplikat
4. Idempotency-Keys: POST sicher machen
Der Idempotency-Key ist das Standardmuster, um POST-Requests idempotent zu machen. Der Client generiert vor dem ersten Versuch eine einmalige UUID und sendet sie als HTTP-Header Idempotency-Key: <uuid>. Der Server speichert den Key zusammen mit dem Ergebnis der Operation in einem schnellen Store (Redis eignet sich hervorragend). Bei einem Retry mit demselben Key gibt der Server einfach das gecachte Ergebnis zurück, ohne die Operation erneut auszuführen.
Die Implementierung auf Serverseite muss atomisch sein: Prüfen ob der Key bereits existiert und Speichern des neuen Keys müssen in einem einzigen atomischen Schritt passieren, um Race Conditions bei parallelen Requests mit demselben Key zu vermeiden. Redis bietet dafür SET key value NX EX ttl: Setze den Key nur wenn er nicht existiert (NX), mit einer TTL. Stripe, Adyen und viele andere Payment-APIs nutzen exakt dieses Muster. Der Key hat eine Gültigkeitsdauer (typisch 24 Stunden), danach kann derselbe Request erneut eine neue Operation auslösen.
Wichtig: Der Idempotency-Key schützt gegen Doppelausführung, nicht gegen falsche Inputs. Wenn der erste Request mit einem bestimmten Key eine Bestellung für Produkt 15 anlegt, und ein späterer Request mit demselben Key Produkt 16 sendet, gibt der Server das gecachte Ergebnis für Produkt 15 zurück – und ignoriert den abweichenden Body. Manche Implementierungen prüfen, ob Body und Key übereinstimmen, und antworten mit 422 bei Abweichung. Das ist die sicherere Variante.
5. PATCH-Semantik: partiell und idempotent zugleich?
PATCH sendet – anders als PUT – nur die zu ändernden Felder. Das macht PATCH für partiell Updates effizienter, aber die Idempotenz ist nicht garantiert. Es hängt von der Semantik des Patches ab. Ein PATCH, der ein Feld auf einen absoluten Wert setzt ({"email": "neue@email.de"}), ist idempotent: Zehnmal ausgeführt ist die E-Mail immer noch dieselbe. Ein PATCH, der inkrementiert ({"quantity": "+1"} oder JSON Patch mit "op": "add" auf eine Array-Eigenschaft), ist nicht idempotent.
JSON Patch (RFC 6902) und JSON Merge Patch (RFC 7396) sind die zwei standardisierten PATCH-Formate. JSON Merge Patch ist das einfachere: Es sendet ein JSON-Objekt, das mit der Ressource zusammengeführt wird. Felder auf null setzen entfernt sie. JSON Patch ist mächtiger und beschreibt Operationen (add, remove, replace, move, copy, test) auf dem JSON-Dokument – ähnlich wie ein Git-Diff. Die test-Operation ermöglicht precondition checks: "Wende diesen Patch nur an, wenn das Feld X den Wert Y hat".
6. Retry-Strategien: Exponential Backoff und Jitter
Ein Retry, der sofort nach dem ersten Fehler wieder sendet, verschlimmert überlastete Server. Das Standardmuster für Retries in verteilten Systemen ist Exponential Backoff: Nach dem ersten Fehler wartet der Client 1 Sekunde, nach dem zweiten 2 Sekunden, nach dem dritten 4 Sekunden, usw. Die Wartezeit wächst exponentiell, bis zu einem konfigurierten Maximum (z.B. 60 Sekunden). Das gibt einem überlasteten Server Zeit zur Erholung, bevor neue Requests ankommen.
Jitter verhindert das Thundering-Herd-Problem: Wenn viele Clients gleichzeitig einen Fehler bekommen und alle nach exakt 2 Sekunden wiederholen, trifft der Server wieder gleichzeitig viele Requests. Jitter fügt eine zufällige Variation zur Wartezeit hinzu: statt 2 Sekunden wartet jeder Client zwischen 1 und 3 Sekunden. AWS empfiehlt "Full Jitter": sleep(random(0, min(cap, base * 2^attempt))). Kombiniert mit Idempotency-Keys bei POST-Requests ist Exponential Backoff mit Jitter die Standardlösung für robuste REST-API-Clients.
#!/usr/bin/env bash
# Retry with exponential backoff + full jitter — safe for idempotent methods
set -euo pipefail
retry_request() {
local method="$1"
local url="$2"
local body="${3:-}"
local max_attempts=5
local base_wait=1
local cap=60
local attempt=0
while (( attempt < max_attempts )); do
http_code=$(curl -s -o /tmp/response_body -w "%{http_code}" \
-X "$method" "$url" \
${body:+-H "Content-Type: application/json" -d "$body"})
# Success: 2xx
if [[ "$http_code" =~ ^2 ]]; then
cat /tmp/response_body
return 0
fi
# Client error: do NOT retry (400, 401, 403, 404, 422)
if [[ "$http_code" =~ ^4 ]] && [[ "$http_code" != "429" ]]; then
echo "[ERROR] Client error $http_code — no retry" >&2
return 1
fi
# 429 or 5xx: retry with exponential backoff + full jitter
(( attempt++ ))
max_sleep=$(( base_wait * (2 ** attempt) ))
(( max_sleep > cap )) && max_sleep=$cap
jitter=$(( RANDOM % max_sleep + 1 ))
echo "[RETRY] Attempt $attempt/$max_attempts — HTTP $http_code — waiting ${jitter}s" >&2
sleep "$jitter"
done
echo "[FAIL] Max retries exceeded" >&2
return 1
}
# Safe to retry: GET is idempotent
retry_request GET "https://api.mironsoft.de/orders/42"
# Safe to retry: PUT is idempotent
retry_request PUT "https://api.mironsoft.de/orders/42" '{"status":"confirmed"}'
7. Optimistic Locking mit ETag und If-Match
ETags (Entity Tags) sind ein HTTP-Mechanismus für Cache-Validierung und Optimistic Locking. Bei einem GET-Request gibt der Server einen ETag-Header zurück: einen Hash oder Versions-Token der Ressource. Bei einem nachfolgenden PUT oder PATCH sendet der Client diesen ETag im If-Match-Header mit. Der Server prüft, ob der ETag noch aktuell ist. Wenn ja, führt er die Operation aus. Wenn nein – weil ein anderer Client die Ressource inzwischen geändert hat – antwortet er mit 412 Precondition Failed.
Dieses Muster verhindert verlorene Updates: Wenn zwei Clients gleichzeitig denselben Benutzer lesen, bearbeiten und zurückschreiben, überschreibt der zweite nicht unbemerkt die Änderungen des ersten. Der zweite Client bekommt 412 und muss die Ressource neu laden, seine Änderungen auf der aktuellen Version wiederholen und erneut senden. ETags sind auch für Conditional GET relevant: If-None-Match erlaubt es Clients, nur dann die vollständige Antwort zu empfangen, wenn sich die Ressource geändert hat – andernfalls antwortet der Server mit 304 Not Modified ohne Body.
8. DELETE-Edge-Cases: 404 als Erfolg behandeln
Die HTTP-Spezifikation legt für DELETE nicht fest, wie ein wiederholtes DELETE auf eine bereits gelöschte Ressource beantwortet werden soll. Manche APIs antworten mit 204 No Content (Ressource ist weg, alles gut), andere mit 404 Not Found. Für idempotente Retry-Logik ist das relevant: Ein Client, der DELETE wiederholt und 404 bekommt, sollte das als Erfolg werten, nicht als Fehler. Das Ziel war, die Ressource zu löschen – und sie ist weg.
Die Empfehlung für API-Designer: DELETE sollte bei einer bereits gelöschten Ressource 404 zurückgeben, weil das der ehrlichere Statuscode ist. Clients sollten in ihrer Retry-Logik für DELETE 204 und 404 beide als Erfolg behandeln. Eine Alternative ist, DELETE immer 204 zurückzugeben – auch wenn nichts gelöscht wurde. Das vereinfacht die Client-Logik, ist aber weniger informativ. In jedem Fall sollte die Retry-Logik für DELETE keinen Fehler bei 404 werfen.
9. HTTP-Methoden im Idempotenz-Vergleich
| Methode | Idempotent? | Sicher? | Retry sicher? | Absicherung |
|---|---|---|---|---|
| GET | Ja | Ja | Ja | Keine nötig |
| PUT | Ja | Nein | Ja | ETag / If-Match gegen Lost Updates |
| DELETE | Ja | Nein | Ja (404 = OK) | 404 als Erfolg behandeln |
| POST | Nein | Nein | Nur mit Key | Idempotency-Key (UUID im Header) |
| PATCH | Abhängig | Nein | Nur bei absoluten Werten | ETag + If-Match, semantisch prüfen |
Die Tabelle verdeutlicht: GET und DELETE sind in ihrer Eigenschaft als idempotente Methoden eindeutig. PUT ist idempotent, aber Optimistic Locking mit ETag schützt zusätzlich vor konkurrierenden Änderungen. POST ist grundsätzlich nicht idempotent, aber durch den Idempotency-Key-Mechanismus sicher machbar. PATCH hängt von der Semantik des konkreten Patches ab – Inkrementierungen sind nicht idempotent, absolute Wertzuweisungen schon.
Mironsoft
REST API Design, Idempotenz-Implementierung und Retry-Strategien
REST-APIs, die Netzwerkfehler überleben?
Wir implementieren Idempotency-Keys, Optimistic Locking und sichere Retry-Strategien für REST-APIs, die auch unter Netzwerkproblemen und Timeouts zuverlässig funktionieren.
Idempotency-Keys
Redis-basierte Implementierung für sichere POST-Wiederholungen ohne Doppelbuchungen
Optimistic Locking
ETag und If-Match für concurrent-update-sichere PUT- und PATCH-Operationen
Retry-Logik
Exponential Backoff mit Jitter für robuste API-Clients in verteilten Systemen
10. Zusammenfassung
Idempotenz ist eine der wichtigsten Eigenschaften für zuverlässige REST-APIs in verteilten Systemen. GET, PUT und DELETE sind von Natur aus idempotent und können bei Netzwerkfehlern oder Timeouts sicher wiederholt werden. POST ist nicht idempotent und erfordert Idempotency-Keys, wenn Retries sicher sein müssen. PATCH hängt von der konkreten Patch-Semantik ab. Exponential Backoff mit Jitter ist das Standardmuster für Retry-Logik, das überlastete Server schützt und das Thundering-Herd-Problem vermeidet.
Die Implementierung von Idempotency-Keys mit Redis ist keine Rocket Science, aber erfordert atomische Operationen, eine klare TTL-Strategie und eine durchdachte Fehlerbehandlung bei Body-Abweichungen. Optimistic Locking mit ETag und If-Match schützt zusätzlich vor Lost Updates bei konkurrierenden Änderungen. Diese Muster zusammen machen REST-APIs zu zuverlässigen Bausteinen in Microservice-Architekturen, wo Netzwerkpartitionen und Timeouts zum Alltag gehören.
Idempotenz und Retry — Das Wichtigste auf einen Blick
Idempotente Methoden
GET, PUT, DELETE sind von Natur aus idempotent und können bei Timeout sicher wiederholt werden. POST ist es nicht – hier Idempotency-Key einsetzen.
Idempotency-Key
UUID im Header, serverseitig atomisch mit Redis gespeichert. Bei Retry: gecachtes Ergebnis zurückgeben. TTL typisch 24 Stunden. Wird von Stripe, Adyen u.a. genutzt.
Retry-Strategie
Exponential Backoff + Full Jitter. 4xx außer 429 nicht retrien. 5xx und 429 retrien. Maximale Versuche und Cap-Wert konfigurieren.
Optimistic Locking
ETag bei GET empfangen, If-Match bei PUT/PATCH mitsenden. 412 = konkurrierendes Update, neu laden und erneut versuchen. Schützt vor Lost Updates.