REST API Idempotency Keys
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).