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
| Strategy | How It Works | Best For |
|---|---|---|
| Database unique constraint | Reject duplicate inserts at the DB level | Record creation |
| Check-before-write | Query for existing record before inserting | General purpose |
| Upsert | Insert or update in a single atomic operation | Status updates |
| Idempotency key table | Store processed keys with TTL | API 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