{ }
GET
REST API · Idempotenz · HTTP · Retry-Strategien
Idempotenz und Retry
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.

14 Min. Lesezeit Idempotency-Key · Exponential Backoff · Optimistic Locking · ETag HTTP · REST · API Design

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.

11. FAQ: Idempotenz und Retry in REST-APIs

1Was bedeutet Idempotenz in REST?
Idempotente Operationen liefern nach der ersten Ausführung denselben Serverzustand – egal wie oft wiederholt. GET, PUT, DELETE idempotent. POST nicht.
2Warum ist POST nicht idempotent?
Jedes POST erstellt eine neue Ressource oder löst eine Aktion aus. Retry ohne Schutz = Doppelbuchung, doppelte Bestellung, zweifache Zahlung.
3Was ist ein Idempotency-Key?
UUID im HTTP-Header, serverseitig mit Redis gespeichert. Bei Retry: gecachtes Ergebnis zurückgeben. Kein Duplikat, keine doppelte Aktion.
4Wie implementiert man Idempotency-Keys?
Redis SET key value NX EX ttl: atomisch, nur wenn Key nicht existiert. Race Conditions vermieden. Vorhandener Key = gecachtes Ergebnis zurückgeben.
5Was ist Exponential Backoff?
Wartezeit wächst exponentiell: 1s, 2s, 4s... bis zum Cap. Schützt überlastete Server. Immer mit Jitter kombinieren gegen Thundering Herd.
6Was ist Jitter bei Retries?
Zufällige Variation der Wartezeit. Verhindert, dass viele Clients gleichzeitig retrien. Full Jitter: random(0, min(cap, base * 2^attempt)).
7Ist PUT immer idempotent?
Ja, wenn es die vollständige Ressource ersetzt. Gegen konkurrierende Updates hilft ETag + If-Match (Optimistic Locking).
8404 bei DELETE — Fehler oder Erfolg?
Erfolg. Ressource ist weg – das war das Ziel. Retry-Logik für DELETE sollte 204 und 404 beide als Erfolg werten.
9Was ist Optimistic Locking mit ETag?
ETag bei GET empfangen, If-Match bei PUT/PATCH mitsenden. 412 = konkurrierendes Update. Ressource neu laden und erneut versuchen.
10PATCH vs. PUT – wann was?
PATCH für partielle Updates einzelner Felder. PUT für vollständige Ressourcenersetzung. PATCH-Idempotenz hängt von der Semantik ab: absoluter Wert = idempotent, Inkrementierung = nicht idempotent.