A POST endpoint for payments or orders with no idempotency protection turns a network retry into a duplicate charge or duplicate resource creation. This is not a theoretical risk — mobile networks, load balancers, and client libraries all retry on connection failure. CWE-770 (Allocation of Resources Without Limits or Throttling) applies when duplicate submissions trigger duplicate downstream effects. PUT endpoints that increment rather than set are silently non-idempotent: calling them twice produces different state than calling them once, making any retry logic dangerous.
High because non-idempotent POST operations on payments or orders create duplicate charges or records on network retries, a real production failure mode on unreliable connections.
Ensure PUT and DELETE are idempotent by design, and add idempotency key support for critical POST operations:
// PUT must SET, not INCREMENT:
// Before (not idempotent):
await db.user.update({ credits: { increment: amount } })
// After (idempotent):
await db.user.update({ credits: amount })
// Idempotency key for critical POST operations:
const idempotencyKey = req.headers.get('Idempotency-Key')
if (idempotencyKey) {
const existing = await db.idempotencyLog.findUnique({ where: { key: idempotencyKey } })
if (existing) return Response.json(existing.response, { status: existing.status })
}
// Process, then store: await db.idempotencyLog.create({ key, response, status })
DELETE must return 204 on the first call and 204 or 404 on subsequent calls — never 500.
ID: api-design.developer-ergonomics.idempotency
Severity: high
What to look for: Check that idempotent HTTP methods behave idempotently. PUT should replace the full resource -- calling it twice with the same data should produce the same result. DELETE should succeed (or return 404/410) on repeated calls. For POST (which is not naturally idempotent), check whether an idempotency key mechanism exists for operations where duplicate submissions are dangerous (payments, orders, invitations). Look for: Idempotency-Key header handling, client-generated request IDs, deduplication logic.
Pass criteria: Count all PUT, DELETE, and critical POST endpoints. At least 100% of PUT and DELETE operations must be idempotent (repeated identical calls produce the same outcome). For critical POST operations (payments, resource creation with side effects), either an idempotency key mechanism exists or the endpoint has deduplication logic to prevent double-processing.
Fail criteria: PUT operations are not idempotent (e.g., incrementing a counter instead of setting a value). DELETE operations return an error on second call (should return 204 or 404, not 500). Critical POST operations (payments, orders) have no idempotency protection -- duplicate submissions create duplicate resources.
Skip (N/A) when: The API only has GET endpoints (read-only). Also skip if no critical write operations exist (no payments, no order creation, no resource creation with side effects).
Detail on fail: Identify the issue (e.g., "PUT /api/users/:id/credits adds to the balance instead of setting it -- not idempotent. POST /api/payments has no idempotency key support -- network retries could create duplicate charges."). Max 500 chars.
Remediation: Ensure PUT and DELETE are idempotent by design. Add idempotency keys for critical POST operations:
// PUT should SET, not INCREMENT:
// Before (not idempotent):
await db.user.update({ credits: { increment: amount } })
// After (idempotent):
await db.user.update({ credits: amount })
// Idempotency key for POST:
const idempotencyKey = req.headers.get('Idempotency-Key')
if (idempotencyKey) {
const existing = await db.idempotencyLog.findUnique({ where: { key: idempotencyKey } })
if (existing) return Response.json(existing.response, { status: existing.status })
}
// Process request, then store result with idempotencyKey