patternjavascriptCritical
Idempotency keys: safely retrying non-idempotent operations
Viewed 0 times
idempotencyidempotency keydouble chargesafe retrypaymentdistributed systems
Problem
A payment request times out. Was it processed or not? Retrying the same request risks charging the customer twice.
Solution
Generate and store idempotency keys with operations:
// Client: generate a stable key for each logical operation
const idempotencyKey = `pay_${orderId}_${userId}`;
// Send with request
const response = await fetch('/api/payments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({ amount: 1999, currency: 'USD' }),
});
// Server: check key before processing
app.post('/api/payments', async (req, res) => {
const key = req.headers['idempotency-key'];
if (!key) return res.status(400).json({ error: 'Idempotency-Key header required' });
// Check if this key was already processed
const existing = await db.query(
'SELECT response FROM idempotency_keys WHERE key = $1 AND expires_at > NOW()',
[key]
);
if (existing.rows[0]) {
return res.status(200).json(existing.rows[0].response); // Return cached result
}
// Process payment
const result = await processPayment(req.body);
// Store result with TTL
await db.query(
"INSERT INTO idempotency_keys (key, response, expires_at) VALUES ($1, $2, NOW() + INTERVAL '24 hours')",
[key, result]
);
res.status(200).json(result);
});Why
Idempotency keys make non-idempotent operations safe to retry by caching the result of the first successful execution and returning it on subsequent requests with the same key.
Gotchas
- The idempotency key must be stored atomically with the operation — use a transaction
- If two requests with the same key arrive simultaneously, use a database lock or unique constraint to prevent both from proceeding
- Expire keys after a reasonable TTL (24 hours to 30 days) to prevent unbounded storage growth
- The Stripe API uses Idempotency-Key as the header name — standardize on this
Revisions (0)
No revisions yet.