Why Idempotency Matters
When building APIs, network failures are inevitable. A client sends a request, the server processes it, but the response is lost. The client retries, and suddenly you have duplicate orders, double charges, or duplicated database records. Idempotency solves this by ensuring that repeating the same request has the same effect as making it once.
An operation is idempotent if making multiple identical requests produces the same result as a single request. GET, PUT, DELETE are naturally idempotent in REST, but POST is not. That's where you need to add explicit support.
The Idempotency Key Pattern
The standard approach is to have the client generate a unique idempotency key for each operation and send it in a header. The server stores the key and the response for the first request. If the key is seen again, the server returns the stored response without re-executing the operation.
Here's a simple implementation in Node.js with Express and Redis:
const express = require('express');
const crypto = require('crypto');
const redis = require('redis');
const app = express();
const client = redis.createClient();
app.use(express.json());
app.post('/orders', async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];
if (!idempotencyKey) {
return res.status(400).json({ error: 'Missing idempotency-key header' });
}
const cacheKey = `idem:${idempotencyKey}`;
// Check if we've seen this key before
const cached = await client.get(cacheKey);
if (cached) {
return res.status(200).json(JSON.parse(cached));
}
// Process the order (e.g., create in DB)
const order = {
id: crypto.randomUUID(),
...req.body
};
// Store the response with a TTL (e.g., 24 hours)
await client.set(cacheKey, JSON.stringify(order), 'EX', 86400);
res.status(201).json(order);
});
Key points:
The client must generate a unique key for each logical operation. UUIDs are a good choice.
Store the response, not just a flag, so retries can return the exact same result.
Use a TTL to avoid unbounded storage.
Handling Concurrent Requests
Race conditions can occur if two identical requests arrive simultaneously. Both might check the cache, find nothing, and process the operation twice. Use atomic operations to prevent this.
With Redis, you can use SET NX (set if not exists):
const setIfNotExists = await client.set(cacheKey, 'processing', 'NX', 'EX', 86400);
if (!setIfNotExists) {
// Key exists, maybe still processing or done
const cached = await client.get(cacheKey);
if (cached === 'processing') {
// Wait and retry, or return 409 Conflict
return res.status(409).json({ error: 'Request already in progress' });
}
return res.status(200).json(JSON.parse(cached));
}
// Process the order...
const order = { id: crypto.randomUUID(), ...req.body };
await client.set(cacheKey, JSON.stringify(order), 'EX', 86400);
res.status(201).json(order);
If the first request is still processing, returning 409 tells the client to retry after a short delay. Many clients handle this well.
What Key Should the Client Use?
Never reuse the same key for different operations. For example, if you're creating a payment, generate a new UUID for each payment attempt. If the client retries the same payment, it sends the same key. If it's a genuinely new payment, it uses a new key.
Some APIs derive keys from a unique business identifier, like an order number. That works if you're sure that order number will never be used again for a different operation.
Idempotency for Non-POST Methods
While PUT and DELETE are idempotent by nature, you still need to handle retries properly. For example, if a DELETE request times out but the server actually deleted the resource, a retry should not return an error. Design your endpoints to treat a missing resource as success for DELETE.
Practical Tips
Always require the Idempotency-Key header for state-changing operations. Return 400 if missing.
Validate key format (e.g., max length 255, UUID).
Include the idempotency key in your API documentation and client SDKs.
Log idempotency key usage for debugging.
For multi-step processes, consider using idempotency keys at each step.
Testing Idempotency
Write tests that simulate retries. Send the same request twice and assert that only one resource is created and both responses are identical. Also test concurrent requests to ensure no race conditions occur.
Conclusion
Idempotency is not optional for production APIs that handle money, orders, or any state changes. It's a simple pattern that saves your users from painful error handling and your database from duplicates. Start with the idempotency key header, store responses, handle concurrency atomically, and you'll have a much more reliable API.
Remember: the goal is that a retry should never cause unintended side effects. Your clients will thank you.
I asked a coding assistant to draft an Nginx config for a small Laravel app on an Ubuntu VPS.
It produced something usable in seconds.
That is the useful part. The dangerous part is that a config can look completely reasonable, pass a quick visual scan, and still be wrong for the actual server.
T
The dangerous version of an AI agent is not the one that gives a bad answer.
It is the one that confidently calls a tool, updates the wrong record, sends the wrong email, retries itself into a loop, and leaves no recoverable trail.
A prototype agent can be impressive and still be unsafe. A product
Every backend engineer has had this conversation at least once: retention gets cut from 90 days to 30, or verbosity gets dialed down in production, because the storage bill for logs quietly became a line item someone in finance started asking about. It usually gets framed as a tooling problem — "we