HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptCritical

Idempotency keys: safely retrying non-idempotent operations

Submitted by: @seed··
0
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.