logo

Idempotency

An operation is idempotent if performing it multiple times produces the same effect as performing it once. In distributed systems, idempotency ensures that retries, duplicate webhook deliveries, and network replays don’t cause unintended side effects like double charges or duplicate records.

Why Idempotency Matters

In any system with retries or at-least-once delivery, your endpoint will receive the same request more than once. Without idempotency:

  • A payment gets charged twice
  • A notification is sent three times
  • A database record is duplicated
  • An inventory count is decremented multiple times

How to Implement Idempotency

The most common approach is an idempotency key — a unique identifier included with each request:

app.post('/api/charge-payment', async (req, res) => {
  const { orderId, amount } = req.body;

  // Use orderId as the idempotency key
  const existing = await db.payments.findByOrderId(orderId);
  if (existing) {
    // Already processed — return the same result
    return res.json({ paymentId: existing.id, status: 'already_processed' });
  }

  const payment = await stripe.charges.create({
    amount,
    idempotency_key: orderId,
  });

  await db.payments.create({ orderId, paymentId: payment.id });
  res.json({ paymentId: payment.id, status: 'charged' });
});

Idempotency Strategies

StrategyHow It WorksBest For
Database unique constraintReject duplicate inserts at the DB levelRecord creation
Check-before-writeQuery for existing record before insertingGeneral purpose
UpsertInsert or update in a single atomic operationStatus updates
Idempotency key tableStore processed keys with TTLAPI endpoints

Idempotency in AsyncQueue

When AsyncQueue retries a task, it sends the same payload to your callback URL. Design your handlers to be idempotent so retries are always safe:

  • Include a unique task or job ID in the payload
  • Check if the work was already completed before executing
  • Use database constraints to prevent duplicate records