Idempotenzschlüssel für sichere API-Schreibvorgänge auf Datenbankebene
AI generated
SELECT
JOIN
SQL / API-Sicherheit
Idempotenzschlüssel für sichere Schreibvorgänge
wie ein UNIQUE-Constraint doppelte Verarbeitung bei Retries verhindert

Ein Netzwerk-Timeout beim Aufruf einer Zahlungs- oder Bestell-API sagt nichts darüber aus, ob der Schreibvorgang auf dem Server tatsächlich fehlgeschlagen ist. Ein naiver Retry-Mechanismus riskiert deshalb doppelte Buchungen, doppelte Bestellungen oder doppelt versendete Benachrichtigungen. Ein Idempotenzschlüssel löst dieses Problem nicht in der Anwendungslogik, sondern strukturell auf Datenbankebene, mit einem UNIQUE-Constraint, der doppelte Verarbeitung selbst bei paralleler Zustellung derselben Anfrage zuverlässig verhindert. Dieser Artikel zeigt das praktische Tabellendesign, Response-Snapshots und eine saubere Aufräumstrategie.

10 Min. Lesezeit Idempotency-Key-Pattern Retry-Sicherheit

1. Warum Netzwerk-Retries doppelte Schreibvorgänge erzeugen können

Ein Client, der einen Schreib-Request an eine API schickt, kann bei einem Timeout oder Verbindungsabbruch nicht unterscheiden, ob der Request den Server nie erreicht hat, ob er erreicht und erfolgreich verarbeitet wurde, die Antwort aber verloren ging, oder ob die Verarbeitung mittendrin fehlgeschlagen ist. Aus Sicht des Clients ist ein automatischer Retry die einzig sinnvolle Reaktion auf einen Timeout, denn ein dauerhaft verlorener Request wäre für viele Geschäftsprozesse inakzeptabel.

Ohne zusätzliche Absicherung führt genau dieser Retry im zweiten der drei genannten Fälle zu einer zweiten, vollständig unabhängigen Verarbeitung derselben fachlichen Operation. Bei einer Zahlung bedeutet das eine doppelte Abbuchung, bei einer Bestellung eine doppelte Lieferung, bei einer Benachrichtigung eine doppelte Nachricht an den Kunden. Diese Fälle sind in der Praxis nicht selten, sondern eine direkte, statistisch vorhersagbare Folge jedes Systems mit Netzwerkkommunikation und Retry-Logik.

2. Das Grundprinzip: ein eindeutiger Schlüssel pro logischer Operation

Ein Idempotenzschlüssel ist ein vom Client generierter, eindeutiger Wert, typischerweise ein UUID, der eine einzelne logische Operation kennzeichnet, unabhängig davon, wie oft der zugehörige HTTP-Request tatsächlich übertragen wird. Der Client erzeugt diesen Schlüssel einmal pro fachlicher Aktion, etwa einmal pro Klick auf den Bestell-Button, und sendet ihn bei jedem Retry-Versuch identisch mit.

Der Server prüft bei jedem eingehenden Request zuerst, ob dieser Idempotenzschlüssel bereits verarbeitet wurde. Ist das der Fall, wird die ursprüngliche, bereits erzeugte Antwort erneut zurückgegeben, ohne die fachliche Operation ein zweites Mal auszuführen. Ist der Schlüssel neu, wird die Operation regulär ausgeführt und das Ergebnis unter diesem Schlüssel gespeichert.

3. Praktisches Tabellendesign: Schlüssel, Status und Response-Snapshot

Die zentrale Absicherung ist ein UNIQUE-Constraint auf der Idempotenzschlüssel-Spalte, kombiniert mit dem Kunden- oder Mandantenkontext, damit derselbe clientseitig erzeugte Schlüssel nicht versehentlich über verschiedene Mandanten hinweg kollidiert. Neben dem Schlüssel selbst speichert die Tabelle den Verarbeitungsstatus, damit parallel eintreffende Retries erkennen können, ob eine Verarbeitung bereits läuft, bereits abgeschlossen ist oder fehlgeschlagen ist.

Zusätzlich wird ein Snapshot der ursprünglichen Antwort gespeichert, typischerweise als JSON-Spalte mit Statuscode und Response-Body. Nur so kann bei einem erneuten Request mit demselben Schlüssel exakt dieselbe Antwort zurückgegeben werden, die der Client beim ersten, für ihn verlorenen Versuch eigentlich erhalten sollte.


CREATE TABLE idempotency_key (
    idempotency_key UUID NOT NULL,
    mandant_id      BIGINT NOT NULL,
    status          TEXT NOT NULL CHECK (status IN ('in_bearbeitung', 'abgeschlossen', 'fehlgeschlagen')),
    request_hash    TEXT NOT NULL,
    response_status INT,
    response_body   JSONB,
    erstellt_am     TIMESTAMPTZ NOT NULL DEFAULT now(),
    laeuft_ab_am    TIMESTAMPTZ NOT NULL,
    PRIMARY KEY (idempotency_key, mandant_id)
);

4. Race Conditions bei parallel eintreffenden identischen Requests

Netzwerk-Retries treffen nicht immer sauber nacheinander ein. Ein Client kann bei einem sehr langen Timeout einen zweiten Versuch starten, während der erste Request noch in Bearbeitung ist, sodass beide Requests praktisch gleichzeitig beim Server ankommen. Ohne zusätzliche Absicherung könnten dann beide Requests parallel prüfen, dass der Schlüssel noch nicht existiert, und beide mit der fachlichen Verarbeitung beginnen.

Der UNIQUE-Constraint löst genau dieses Problem strukturell: Der erste Request, der versucht, die Zeile mit dem Idempotenzschlüssel einzufügen, gewinnt das Insert, der zweite, parallel eintreffende Request erhält vom Datenbanksystem einen Constraint-Verletzungsfehler und weiß dadurch zuverlässig, dass bereits eine Verarbeitung läuft oder abgeschlossen ist, ganz ohne expliziten Lock oder Anwendungscode, der Race Conditions selbst auflösen müsste.


-- Insert versuchen; bei Constraint-Verletzung existiert
-- bereits ein Eintrag für diesen Idempotenzschlüssel
INSERT INTO idempotency_key
    (idempotency_key, mandant_id, status, request_hash, laeuft_ab_am)
VALUES ($1, $2, 'in_bearbeitung', $3, now() + interval '24 hours')
ON CONFLICT (idempotency_key, mandant_id) DO NOTHING
RETURNING idempotency_key;

5. Warum ein Request-Hash gegen Schlüssel-Wiederverwendung schützt

Ein Idempotenzschlüssel garantiert nur, dass dieselbe logische Operation nicht doppelt ausgeführt wird, nicht aber, dass ein Client den Schlüssel korrekt verwendet. Würde derselbe Schlüssel versehentlich oder böswillig für eine inhaltlich andere Anfrage wiederverwendet, gäbe die API fälschlich die alte, zum ursprünglichen Request passende Antwort zurück, obwohl der neue Request eigentlich verarbeitet werden sollte.

Ein gespeicherter Hash über die relevanten Request-Parameter, etwa Betrag und Empfänger bei einer Zahlung, erlaubt es, bei jedem Request mit bekanntem Schlüssel zu prüfen, ob der Inhalt tatsächlich identisch ist. Weicht der Hash ab, kann die API einen expliziten Konfliktfehler zurückgeben, statt stillschweigend eine falsche, veraltete Antwort auszuliefern.

6. Umgang mit fehlgeschlagener Verarbeitung und offenen Zuständen

Schlägt die fachliche Verarbeitung nach dem erfolgreichen Insert des Idempotenzschlüssels fehl, etwa weil eine nachgelagerte Zahlungsabwicklung einen Fehler zurückgibt, muss der Status konsequent auf einen fehlgeschlagenen Zustand aktualisiert werden. Ein nachfolgender Retry mit demselben Schlüssel kann dann bewusst entscheiden, ob ein erneuter Versuch zulässig ist oder ob der fehlgeschlagene Zustand dauerhaft zurückgegeben werden soll.

Bleibt ein Eintrag dauerhaft im Status einer laufenden Verarbeitung hängen, etwa weil der verarbeitende Prozess abgestürzt ist, ohne den Status zu aktualisieren, braucht das System eine Timeout-Logik, die einen als hängend erkannten Eintrag nach einer angemessenen Frist als fehlgeschlagen markiert und damit einen erneuten Versuch wieder zulässt, statt den Client dauerhaft zu blockieren.

7. Aufräumstrategie: warum Idempotenzeinträge nicht ewig leben dürfen

Idempotenzschlüssel müssen nur so lange gespeichert bleiben, wie realistische Retry-Versuche eines Clients zu erwarten sind, üblicherweise ein Zeitraum von Stunden bis wenigen Tagen, nicht dauerhaft. Eine unbegrenzt wachsende Idempotenz-Tabelle vergrößert nicht nur den Speicherbedarf, sondern auch die Größe des zugehörigen UNIQUE-Index und verlangsamt dadurch mit der Zeit jeden Lookup bei eingehenden Requests.

Die in der Beispieltabelle enthaltene Spalte für das Ablaufdatum ermöglicht ein regelmäßiges Aufräumen per Batch-Job, der abgelaufene Einträge löscht. Ein zusätzlicher Index auf dieser Spalte hält diese Löschoperation auch bei großen Tabellen effizient, und in manchen Datenbanksystemen lässt sich das Ablaufverhalten sogar über eine native TTL-Funktion automatisieren, statt einen eigenen Batch-Job pflegen zu müssen.


-- Regelmäßiger Aufräum-Job für abgelaufene Einträge
DELETE FROM idempotency_key
WHERE laeuft_ab_am < now()
LIMIT 10000;

CREATE INDEX idx_idempotency_expiry ON idempotency_key (laeuft_ab_am);

8. Grenzen des Musters: was ein Idempotenzschlüssel nicht löst

Ein Idempotenzschlüssel schützt ausschließlich vor doppelter Verarbeitung derselben Anfrage, nicht vor fachlich unterschiedlichen, aber inhaltlich äquivalenten Operationen, etwa zwei separaten Bestellungen mit identischem Inhalt, die der Kunde bewusst zweimal auslöst. Auch Nebenwirkungen, die außerhalb der eigenen Transaktion liegen, etwa der Aufruf eines externen Zahlungsdienstleisters, müssen selbst idempotent gestaltet sein oder zusätzlich abgesichert werden, weil der eigene Idempotenzschlüssel diesen externen Aufruf nicht automatisch schützt.

Für verteilte Systeme mit mehreren beteiligten Diensten reicht ein einzelner Idempotenzschlüssel auf einer einzelnen Datenbank deshalb oft nicht aus. Der Schlüssel muss dann konsequent durch die gesamte Aufrufkette weitergereicht werden, damit jeder beteiligte Dienst dieselbe Garantie unabhängig durchsetzen kann.

9. Praxisempfehlung: wo sich der Aufwand konkret lohnt

Idempotenzschlüssel lohnen sich vor allem bei Schreiboperationen mit finanziellen oder anderweitig schwerwiegenden Folgen: Zahlungen, Bestellabschlüsse, Versandauslösungen und ähnliche Operationen, bei denen eine versehentliche Doppelverarbeitung teuer oder für Kunden sichtbar ist. Für rein lesende Operationen oder für Schreibvorgänge, die von Natur aus bereits idempotent sind, etwa das Setzen eines festen Statuswerts, ist der zusätzliche Aufwand meist nicht gerechtfertigt.

Für neue APIs empfiehlt sich, den Idempotenzschlüssel als expliziten, dokumentierten Bestandteil der Schnittstelle einzuführen, etwa als HTTP-Header, statt ihn implizit aus anderen Feldern abzuleiten. Das macht die Garantie für Client-Entwickler sichtbar und verhindert, dass Retry-Sicherheit von zufälligen Implementierungsdetails abhängt.

Aspekt Ohne Idempotenzschlüssel Mit Idempotenzschlüssel Praxisrelevanz
Retry bei Timeout Risiko doppelter Verarbeitung Zweite Anfrage liefert Original-Antwort Direkter Schutz vor Doppelbuchungen
Parallele identische Requests Race Condition möglich UNIQUE-Constraint erzwingt Gewinner Kein zusätzlicher Lock nötig
Fehlerhafte Schlüssel-Wiederverwendung Unerkannt, falsche Antwort möglich Request-Hash deckt Abweichung auf Schützt vor fehlerhafter Client-Implementierung
Speicherwachstum Nicht relevant Aufräum-Job nach Ablaufdatum nötig Sonst wachsender Index, langsamere Lookups
Externe Nebenwirkungen Ungeschützt Muss zusätzlich abgesichert werden Idempotenzschlüssel allein reicht nicht

Mironsoft

Datenbank-Optimierung, Query-Tuning und Migrationen

SQL-Abfragen, die bei Wachstum immer langsamer werden?

Wir analysieren und optimieren SQL-Datenbanken unabhängig vom eingesetzten System, planen sichere Migrationen und Schema-Änderungen und bringen Teams Query-Optimierung praxisnah bei.

Query-Optimierung

Langsame Abfragen analysieren und mit Indizes und Explain-Plänen gezielt beschleunigen.

Migrations-Planung

Schema-Änderungen und Datenmigrationen sicher und ohne Downtime umsetzen.

Team-Schulung

SQL-Grundlagen und Performance-Denken praxisnah im Entwicklerteam verankern.

10. Zusammenfassung

Idempotenzschlüssel in der Praxis

Kernidee

Ein clientseitig erzeugter, eindeutiger Schlüssel pro logischer Operation, abgesichert durch einen UNIQUE-Constraint, verhindert doppelte Verarbeitung bei Netzwerk-Retries strukturell.

Tabellendesign

Schlüssel plus Mandantenkontext als Primärschlüssel, Verarbeitungsstatus und ein JSON-Snapshot der ursprünglichen Antwort, damit Retries exakt dieselbe Antwort erhalten.

Race-Condition-Schutz

Der UNIQUE-Constraint entscheidet bei parallel eintreffenden identischen Requests strukturell den Gewinner, ganz ohne expliziten Anwendungs-Lock.

Aufräumen

Abgelaufene Einträge nach einem realistischen Retry-Zeitfenster per Batch-Job oder nativer TTL-Funktion löschen, um Index-Wachstum und langsamere Lookups zu vermeiden.

11. FAQ: Idempotenzschlüssel in der Praxis

1Was ist ein Idempotenzschlüssel im Kontext einer API?
Ein vom Client generierter, eindeutiger Wert, der eine einzelne logische Schreiboperation kennzeichnet und bei jedem Retry-Versuch identisch mitgesendet wird, damit der Server doppelte Verarbeitung erkennen kann.
2Warum genügt ein einfacher Check vor dem Insert nicht, um Duplikate zu verhindern?
Weil zwischen dem Check und dem eigentlichen Insert eine Race Condition auftreten kann, wenn zwei Requests parallel eintreffen. Nur ein UNIQUE-Constraint auf Datenbankebene löst dieses Problem strukturell und atomar.
3Warum sollte die Antwort des ersten Requests als Snapshot gespeichert werden?
Damit ein Retry mit demselben Idempotenzschlüssel exakt dieselbe Antwort erhält, die beim ersten, für den Client eigentlich verlorenen Versuch zurückgegeben worden wäre, statt eine abweichende oder leere Antwort.
4Wozu dient der zusätzliche Request-Hash in der Idempotenz-Tabelle?
Er erkennt, wenn derselbe Schlüssel versehentlich oder fehlerhaft für eine inhaltlich andere Anfrage wiederverwendet wird, und verhindert, dass die API in diesem Fall stillschweigend eine falsche, veraltete Antwort ausliefert.
5Wie lange sollten Idempotenzschlüssel-Einträge gespeichert bleiben?
Nur so lange, wie realistische Retry-Versuche eines Clients zu erwarten sind, meist Stunden bis wenige Tage. Ein regelmäßiger Aufräum-Job oder eine native TTL-Funktion hält die Tabelle und ihren Index kompakt.
6Schützt ein Idempotenzschlüssel auch externe Aufrufe wie Zahlungsdienstleister?
Nicht automatisch. Nebenwirkungen außerhalb der eigenen Transaktion müssen selbst idempotent gestaltet sein oder der Schlüssel muss konsequent durch die gesamte Aufrufkette weitergereicht werden.
7Was passiert, wenn die fachliche Verarbeitung nach dem Insert fehlschlägt?
Der Status muss auf einen fehlgeschlagenen Zustand aktualisiert werden, damit ein späterer Retry bewusst entscheiden kann, ob ein erneuter Versuch zulässig ist, statt für immer im Status einer laufenden Verarbeitung zu hängen.
8Warum reicht ein einzelner Idempotenzschlüssel in verteilten Systemen manchmal nicht aus?
Weil mehrere beteiligte Dienste jeweils eigene Datenbanken haben können. Der Schlüssel muss dann durch die gesamte Aufrufkette weitergereicht werden, damit jeder Dienst die Garantie unabhängig durchsetzen kann.
9Für welche Operationen lohnt sich ein Idempotenzschlüssel besonders?
Vor allem für Schreiboperationen mit finanziellen oder schwerwiegenden Folgen wie Zahlungen, Bestellabschlüsse und Versandauslösungen, bei denen eine versehentliche Doppelverarbeitung teuer oder für Kunden sichtbar wäre.
10Sollte der Idempotenzschlüssel als HTTP-Header oder als Teil des Request-Bodys übertragen werden?
Ein dedizierter HTTP-Header wird in der Praxis meist bevorzugt, weil er die Garantie als expliziten, dokumentierten Bestandteil der Schnittstelle sichtbar macht, statt sie implizit aus anderen Feldern abzuleiten.