idempotency
Use when creating mutation endpoints. Use when trusting frontend to prevent duplicates. Use when payments or critical operations can be repeated.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when creating mutation endpoints. Use when trusting frontend to prevent duplicates. Use when payments or critical operations can be repeated.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when writing tests. Use when test structure is unclear. Use when arrange/act/assert phases are mixed.
Use when designing or modifying APIs. Use when adding breaking changes. Use when clients depend on API stability.
Use when implementing authentication. Use when storing passwords. Use when asked to store credentials insecurely.
Use when same data is fetched repeatedly. Use when database queries are slow. Use when implementing caching without invalidation strategy.
Use when tempted to use class inheritance. Use when creating class hierarchies. Use when subclass needs only some parent behavior.
Use when acquiring multiple locks. Use when operations wait for each other. Use when system hangs without crashing.
| name | idempotency |
| description | Use when creating mutation endpoints. Use when trusting frontend to prevent duplicates. Use when payments or critical operations can be repeated. |
Critical operations must be safe to retry. Use idempotency keys.
Networks fail. Clients retry. Users double-click. Without idempotency, retries cause duplicate charges, orders, or data corruption.
NEVER rely on frontend to prevent duplicate requests.
No exceptions:
If mutations have no duplicate protection, STOP:
// ❌ VIOLATION: No idempotency protection
app.post('/payments', async (req, res) => {
const { userId, amount, cardToken } = req.body;
// If this request retries, user gets charged twice!
const payment = await stripeCharge(amount, cardToken);
await db.payments.create({ userId, amount, stripeId: payment.id });
res.json({ success: true });
});
What can go wrong:
// ✅ CORRECT: Idempotency key protection
app.post('/payments', async (req, res) => {
// Require idempotency key
const idempotencyKey = req.headers['idempotency-key'];
if (!idempotencyKey) {
return res.status(400).json({
error: 'Idempotency-Key header is required'
});
}
const { userId, amount, cardToken } = validated(req.body);
// Check for existing request with this key
const existing = await db.idempotencyKeys.findOne({
where: { key: idempotencyKey, userId }
});
if (existing) {
// Return cached response
return res.status(existing.statusCode).json(existing.response);
}
try {
// Process the payment
const payment = await stripeCharge(amount, cardToken);
await db.payments.create({ userId, amount, stripeId: payment.id });
const response = { success: true, paymentId: payment.id };
// Cache the response
await db.idempotencyKeys.create({
key: idempotencyKey,
userId,
statusCode: 200,
response,
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
});
res.json(response);
} catch (error) {
// Cache error responses too (optional, depends on error type)
throw error;
}
});
// Client usage:
// POST /payments
// Headers: { "Idempotency-Key": "user-123-order-456-attempt-1" }
// Option 1: UUID per request
const key = crypto.randomUUID();
// Option 2: Deterministic (better for retries)
const key = `${userId}-${orderId}-${timestamp}`;
// Option 3: Hash of request content
const key = hash(JSON.stringify({ userId, items, amount }));
interface IdempotencyRecord {
key: string;
userId: string;
statusCode: number;
response: any;
createdAt: Date;
expiresAt: Date; // Clean up old keys
}
| Operation | Risk | Solution |
|---|---|---|
| Payments | Double charge | Idempotency key |
| Order creation | Duplicate orders | Idempotency key |
| Inventory decrement | Over-decrement | Idempotency key |
| Email sending | Duplicate emails | Idempotency key |
| Account creation | Duplicate accounts | Unique constraint + idempotency |
Pressure: "We disable the button, show loading state"
Response: Networks retry automatically. JavaScript crashes. Users have fast fingers.
Action: Backend idempotency. Frontend UX is not protection.
Pressure: "Duplicates are rare edge cases"
Response: Rare × many users = many angry users. One duplicate charge = support nightmare.
Action: Protect all critical mutations.
Pressure: "Our users are careful"
Response: Users have slow connections. Buttons are small. Frustration leads to clicking.
Action: Never rely on user behavior.
Pressure: "Duplicate insert will fail"
Response: Unique constraint throws error. User sees error. UX is terrible.
Action: Idempotency returns same success response.
All of these mean: Add idempotency protection.
| Unsafe | Safe |
|---|---|
| Trust frontend | Require idempotency key |
| Error on duplicate | Return cached response |
| Assume single request | Design for retries |
| POST = new resource always | POST + key = at-most-once |
| Excuse | Reality |
|---|---|
| "Frontend prevents it" | Networks retry. Users double-click. |
| "Rarely happens" | Rare × scale = many incidents. |
| "Users are careful" | Users are human. |
| "Unique constraint" | Constraints throw errors, not success. |
| "Too complex" | Simpler than handling support tickets. |
Require idempotency keys for all critical mutations.
Never trust frontend protection. Cache responses by idempotency key. Return the same response for duplicate requests. Clean up old keys periodically.