Idempotent APIs in Node.js with Redis
Source: Dev.to
Overview

Distributed systems lie. Requests get retried. Webhooks arrive twice. Clients timeout and try again. What should be a single operation suddenly runs multiple times — and now you’ve double‑charged a customer or processed the same event five times. Idempotency is the fix. Doing it correctly is the hard part.
This post shows how to implement idempotent APIs in Node.js using Redis, and how the idempotency-redis package helps handle retries, payments, and webhooks safely.
What idempotency means for APIs
An API operation is idempotent if multiple calls with the same idempotency key produce the same result — and side effects happen only once.
- One execution per idempotency key
- Concurrent or retried requests replay the same result
- Failures can be replayed too
This matters for:
- 💳 Payments
- 🔁 Automatic retries
- 🔔 Webhooks
- 🧵 Concurrent requests
Why naive solutions fail
Common approaches break down quickly:
- In‑memory locks → don’t work across instances
- Database uniqueness → hard to replay results
- Redis
SETNX→ no result or error replay - Returning
409 Conflict→ pushes complexity to clients
What you actually need is coordination + caching + replay, shared across all nodes.
Using idempotency-redis
idempotency-redis provides idempotent execution backed by Redis:
- One request executes the action
- Others wait and replay the cached result
- Errors are cached and replayed by default
- Works across multiple Node.js instances
Basic example
import Redis from 'ioredis';
import { IdempotentExecutor } from 'idempotency-redis';
const redis = new Redis();
const executor = new IdempotentExecutor(redis);
await executor.run('payment-123', async () => {
return chargeCustomer();
});
Calling this five times concurrently with the same key runs the function once.
Real‑world use cases
Payments
Payment providers and clients retry aggressively. Your API must never double‑charge.
await executor.run(`payment:${paymentId}`, async () => {
const charge = await stripe.charges.create(/* … */);
await saveToDB(charge);
return charge;
});
If the response is lost, retries replay the cached result — no second charge.
Webhooks
Webhook providers explicitly say “events may be delivered more than once.”
await executor.run(`webhook:${event.id}`, async () => {
await processWebhook(event);
});
Duplicate delivery? Same result. One execution.
Retries without fear
With idempotency in place, you can safely:
- Enable HTTP retries
- Retry background jobs
- Handle slow or flaky dependencies
No duplicate work. No race conditions.
Error handling and control
By default, errors are cached and replayed — preventing infinite retries. You can opt out selectively:
await executor.run(key, action, {
shouldIgnoreError: (err) => err.retryable === true
});
When to use this
Use idempotency-redis if you:
- Build APIs that mutate state
- Accept retries or webhooks
- Run multiple Node.js instances
- Care about correctness under failure
Learn more
- 📦 npm:
- 🐙 GitHub: