Blog Hoch
Jan 2026·2 tup leghDe'wI' chenmoHta'

REST API Idempotency Keys

#api#backend#reliability

client payment request nob, network timeout (response qaSpa'), client retry (reasonable). POST /payments endpoint charge redo vaj customer cha'logh bill. Idempotency keys 'oH Qotlh: client request (succeed pagh) retry safely, duplicate side effect pagh.

core idea

idempotency key unique value: client logical operation Hoch wa'logh generate (UUID), attempt Hoch nob (retries je).

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

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

server Qu' simple: key wa'DIch legh vaj operation Qu' 'ej result store (key rap). request same key rap vaj stored result nob (operation redo pagh) - body identical legh 'ach, seconds hours tlhoy' 'ach.

minimal implementation

Node/Express handler database table rap:

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);
}

request_hash check qatlh: client accident pagh malicious key different operation vaD reuse Qotlh. body stored pIm vaj reject (new request silently run pagh old response nob pagh).

race condition

naive version gap: cha' requests same key "exists?" check pass (insert qaSpa'), cha' customer charge. fix: insert atomicity point. key unique constraint, database cha'DIch insert reject:

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;
}

'oH idempotency keys database poQ qatlh - in-memory Map wa' server instance load balancer server law' vaj Qaw', restart mid-request Qaw'.

"operation" nuq

Idempotency keys same request retries protect. client intentionally endpoint cha'logh different keys call pagh. client responsibility: logical action Hoch wa' key (checkout attempt, form submission) - HTTP call Hoch pagh, fixed key unrelated operations vaD pagh.

Qagh: retry Hoch fresh UUID generate (first attempt reuse pagh) - mechanism Qaw'. key wa'logh generate (wa'DIch attempt qaSpa'), retries Hoch Daq carry.

old keys expire

table forever grow pagh. keys retry window neH Dun - background job pagh cron rows 24 hours qan delete:

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

client window qaSDI' same key retry vaj genuine duplicate operation worst case - acceptable (jaj qaSDI' retry same user action pagh).

nuqDaq lo'

endpoint Hoch poQ pagh. GET idempotent by definition - cha'logh run side effects pagh. POST 'ej PATCH endpoints (resources create, real consequences side effects): payments, order placement, email SMS send, infrastructure provision. retry failed request harmless vaj machinery poQ pagh.

yaj

Idempotency keys "that go through'a'?" (timeout qaSDI' client answer laHbe') non-issue choH: same key retry, same result, exactly once. pattern simple, insert atomicity concurrent requests safe (sequential neH pagh).

chu'LRU cache pagh vo' machqanPython3: Dochmey rap pong rarwI' pong choH