Zruck zum Blog, du Zniachtl
Jän. 2026·3 Min. zum LesnVo da KI gmocht

Idempotency Keys in REST-APIs

#api#backend#reliability

A Client schickt a Zahlungsanfrage, des Netzwerk läuft in a Timeout, bevor die Antwort zruckkummt, und da Client probiert's - verständlicherweise - nochamoi. Wenn dein POST /payments-Endpoint die Abbuchung einfach nochamoi ausführt, wird da Kunde zwoamoi belastet. Idempotency Keys lösn genau des Problem: Sie lossn an Client a Anfrage, de möglicherweise erfolgreich woar oda ned, sicher wiederholn, ohne an doppelten Seiteneffekt zu riskiern.

Da Kerngedanke

A Idempotency Key is a eindeutiger Wert, den da Client einmal pro logischer Operation generiert (meist a UUID) und bei jedem Versuch dieser Operation mitschickt, a bei Wiederholungen.

POST /payments
Idempotency-Key: 8f14e45f-ceea-4f3f-9d5f-3b6b1a2c9a10

{ "amount": 4999, "currency": "usd", "customerId": "cus_123" }

Die Orbeit vom Server is im Prinzip einfach: Beim ersten Moi, wo er an bestimmten Key sieht, führt er die Operation aus und speichert das Ergebnis unter diesem Wert. Jede weitere Anfrage mit dem söbn Key gibt das gspeicherte Ergebnis zruck, statt die Operation nochamoi auszuführen - a wenn da Request-Body identisch ausschaut, a wenn er Sekunden oda Stunden später kummt.

A minimale Implementierung

So schaut's aus bei am Node/Express-Handler mit ana Datenbanktabelle dahinter:

CREATE TABLE idempotency_keys (
  key TEXT PRIMARY KEY,
  request_hash TEXT NOT NULL,
  status TEXT NOT NULL, -- 'in_progress' | 'completed'
  response_body JSONB,
  response_status INT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
async function handlePayment(req, res) {
  const key = req.header('Idempotency-Key');
  if (!key) return res.status(400).json({ error: 'Idempotency-Key header required' });

  const requestHash = hashBody(req.body);
  const existing = await db.query('SELECT * FROM idempotency_keys WHERE key = $1', [key]);

  if (existing.rows.length) {
    const record = existing.rows[0];
    if (record.request_hash !== requestHash) {
      return res.status(422).json({ error: 'Idempotency key reused with a different request body' });
    }
    if (record.status === 'completed') {
      return res.status(record.response_status).json(record.response_body);
    }
    return res.status(409).json({ error: 'Request with this key is still processing' });
  }

  await db.query(
    'INSERT INTO idempotency_keys (key, request_hash, status) VALUES ($1, $2, $3)',
    [key, requestHash, 'in_progress'],
  );

  const result = await chargeCustomer(req.body);

  await db.query(
    'UPDATE idempotency_keys SET status = $1, response_body = $2, response_status = $3 WHERE key = $4',
    ['completed', result.body, result.status, key],
  );

  return res.status(result.status).json(result.body);
}

Der request_hash-Check is wichtig: Er hindert an Client dran, versehentlich (oda böswillig) an Key für a wirklich andere Operation wiederzuverwenden. Wenn da Body ned zu dem passt, wos für den Key gspeichert is, lehn ab, statt still den neichn Request auszuführn oda die oide Antwort zruckzugebn.

Die Race Condition, die du behandeln muasst

Die naive Version oben hot a Lücke: Zwoa Requests mit dem söbn Key kinnan beide den "gibt's den scho?"-Check bestehn, bevor oana von beiden a Zeile einfügt, und beide belastn den Kunden. Behebe des, indem du das Einfügen söba zum Atomaritätspunkt machst - nutz an Unique Constraint auf key und lass die Datenbank den zwoatn Insert ablehnen:

try {
  await db.query(
    'INSERT INTO idempotency_keys (key, request_hash, status) VALUES ($1, $2, $3)',
    [key, requestHash, 'in_progress'],
  );
} catch (err) {
  if (err.code === '23505') { // unique_violation
    // Someone else beat us to it — poll or return 409, don't proceed.
    return res.status(409).json({ error: 'Request with this key is already being processed' });
  }
  throw err;
}

Genau deswegn brauchen Idempotency Keys überhaupt a Datenbank (oda Äquivalent) - a In-Memory-Map auf oaner einzigen Server-Instanz rettet di ned, sobois mehr ois oan Server hinter am Load Balancer host, und sie überlebt mit Sicherheit koan Neustart mittn im Request.

Wos als "die Operation" zählt

Idempotency Keys schützn vor Wiederholungen vom söbn Request, ned dagegen, dass da Client den Endpoint absichtlich zwoamoi mit verschiedenen Keys aufruaft. Diese Unterscheidung liegt in da Verantwortung vom Client: Generier oan Key pro logischer Aktion (pro Checkout-Versuch, pro Formularabsendung), ned oan Key pro HTTP-Aufruf, den da Client grod macht, und ned oan festen Key, der für unabhängige Operationen wiederverwendet wird.

A häufiger Fehler is, bei jeder Wiederholung a frische UUID zu generieren, statt die vom ersten Versuch wiederzuverwenden - des macht den ganzen Mechanismus zunichte, weil da Server jede Wiederholung ois brandneuen Key sieht und sie frisch verarbeitet. Da Key muass einmal generiert wern, vor'm ersten Versuch, und durch jede Wiederholung von demselben Versuch mitgenommen wern.

Alte Keys ablaufen lossn

Du wüst ned, dass dera Tabelle ewig wachst. Idempotency Keys sand nur für das Retry-Fenster nützlich, das a Client realistischerweise nutzt - a Hintergrund-Job oda Cron kann Zeiln, de älter sand ois sagn ma 24 Stunden, sicher löschen:

DELETE FROM idempotency_keys WHERE created_at < now() - interval '24 hours';

Wenn a Client irgendwie nach dem Fenster mit dem söbn Key wiederholt, kriagt er im schlimmsten Foi a echte doppelte Operation - a akzeptabler Kompromiss, weil a Wiederholung an Tag später fast sicher nimma die söbe Nutzeraktion is.

Wo des wirklich hingehört

Ned jeder Endpoint braucht des. GET-Requests sand per Definition idempotent - zwoamoi ausführn hot koane Seiteneffekte, also gibt's nix zum Schützn. Die Technik zählt für POST- und manchmal PATCH-Endpoints, de Ressourcen erzeugen oda Seiteneffekte mit echten Konsequenzen auslösen: Zahlungen, Bestellungen, E-Mail- oda SMS-Versand, Infrastruktur-Provisionierung. Wenn a Wiederholung von an fehlgschlogenen Request in beide Richtungen harmlos wär, brauchst die Mechanik ned.

Fazit

Idempotency Keys machen aus "is des wirklich durchgangen?" - a Frage, die dein Client nach am Timeout ned zuverlässig beantworten kann - a Nicht-Problem: Mit dem söbn Key wiederholen, des söbe Ergebnis kriagn, genau einmal. Das Muster is einfach, aber die Atomarität vom Insert is das, wos es unter nebenläufigen Requests tatsächlich sicher mocht, ned nur unter sequentiellen.

NeicherAn LRU-Cache von Grund auf bauenElterPython3: Dateien mit gleichen Namen umbenennen