AI Creates Duplicates on Every Retry
AI generates mutating operations with: no deduplication (retry a POST request, create a duplicate resource), no idempotency keys (the server cannot distinguish a retry from a new request), state-dependent side effects (sending an email on every request — retry = duplicate email), no conditional updates (two concurrent PUTs both overwrite each other — last write wins, data lost), and no awareness of network failure semantics (timeout does not mean failure — the request may have succeeded). Every network timeout is a Schrodinger request: it succeeded and failed until you check.
Modern idempotency is: key-based (Idempotency-Key header identifies retries of the same operation), deduplication-stored (server stores the result keyed by the idempotency key — retries return the stored result), naturally designed (operations are inherently idempotent where possible — PUT replaces, DELETE removes), conditionally updated (ETags and If-Match prevent concurrent overwrites), and lifecycle-managed (keys expire after a TTL, stored results are cleaned up). AI generates none of these.
These rules cover: Idempotency-Key implementation, deduplication stores, naturally idempotent operation design, conditional updates with ETags, and idempotency key lifecycle management.
Rule 1: Idempotency-Key Headers on Mutating Operations
The rule: 'Require an Idempotency-Key header on all POST requests that create resources or trigger side effects. The client generates a unique key per intended operation: Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000. The server: (1) checks if this key has been seen before, (2) if seen: return the stored result (no re-execution), (3) if new: execute the operation, store the result with the key, return the result. Retries with the same key: the server returns the original result without re-executing. The operation happens exactly once.'
For the client contract: 'The client must: generate a new key for each distinct operation (not per request — retries of the same operation use the same key), store the key until the operation is confirmed (retry with the same key on timeout), and never reuse a key for a different operation (the key is bound to one specific intent). Pattern: const key = crypto.randomUUID(); try { await createOrder(data, key); } catch (timeout) { await createOrder(data, key); // same key = safe retry }.'
AI generates: POST /api/orders with no idempotency key. Network timeout: did the order get created? The client does not know. Retry: if the server received the first request, a duplicate order is created. With idempotency key: the retry sends the same key, the server finds the stored result, returns it without creating a second order. The client gets the response it missed. One order, guaranteed.
- Idempotency-Key header on all POST requests that create or mutate
- Client generates UUID per operation: new key = new operation, same key = retry
- Server checks key: seen = return stored result, new = execute and store
- Retry with same key: server returns original result, no re-execution
- Stripe, PayPal, and all major payment APIs require idempotency keys
Without idempotency key: network timeout, client retries, duplicate order created. With key: retry sends the same UUID, server finds the stored result, returns it without re-executing. The order was created once. The client gets the response it missed.
Rule 2: Redis or Database Deduplication Store
The rule: 'Store idempotency key results in Redis (fastest) or a database table (durable). Redis: SET idempotency:{key} {serializedResult} EX 86400 (24-hour TTL). On request: GET idempotency:{key} — if exists, return the stored result. If not: execute the operation, SET the result. Redis is ideal because: sub-millisecond lookups (no latency added to the request path), built-in TTL (automatic cleanup), and atomic SET-if-not-exists (SETNX prevents race conditions between concurrent requests with the same key).'
For database-backed store: 'Table: idempotency_keys { key: text PRIMARY KEY, result: jsonb, status: text (pending/completed/failed), created_at: timestamp, expires_at: timestamp }. On request: INSERT with ON CONFLICT DO NOTHING. If the insert succeeds (new key): execute the operation, UPDATE with the result. If the insert fails (key exists): SELECT the stored result and return it. The database provides durability (results survive Redis restart) and transactional consistency (the operation and the key storage are in the same transaction).'
AI generates: no deduplication store. The server has no memory of previous requests. Every request is treated as new. A retry after timeout: the server executes the operation again because it does not remember the first execution. With a deduplication store: the server remembers every idempotency key for 24 hours. Retries within 24 hours return the stored result. After 24 hours: the key expires, a new request with the same key is treated as new (correct behavior — it is not a retry at that point).
Rule 3: Design Naturally Idempotent Operations
The rule: 'Design operations to be naturally idempotent wherever possible — no idempotency key needed. PUT /api/users/123 { name: "Alice" }: replacing the entire resource is idempotent (doing it twice produces the same state). DELETE /api/users/123: deleting an already-deleted resource returns 204 (or 404 — either way, the resource is gone). PATCH with absolute values: { status: "shipped" } is idempotent (setting to shipped twice = still shipped). PATCH with relative values: { quantity: { increment: 1 } } is NOT idempotent (incrementing twice = +2). Prefer absolute state transitions over relative increments.'
For state machine transitions: 'Model state changes as explicit transitions: order.ship() succeeds if status is 'processing', fails if status is already 'shipped'. The transition checks the current state before applying. Retrying ship() on an already-shipped order: returns success (the desired state is already achieved) or returns a clear message ("Order is already shipped"). The operation is idempotent because the state check prevents double-application. State machine: { processing: ["shipped", "cancelled"], shipped: ["delivered", "returned"] }.'
AI generates: POST /api/orders/123/ship with side effects (send email, update inventory, notify warehouse) that execute on every call. Retry: email sent twice, inventory decremented twice, warehouse notified twice. With natural idempotency: check status before side effects. If already shipped: return success, skip all side effects. The operation produces the same result regardless of how many times it is called.
- PUT (replace) and DELETE are naturally idempotent — no keys needed
- PATCH with absolute values: { status: 'shipped' } is idempotent
- PATCH with increments: { quantity: +1 } is NOT idempotent — needs a key
- State machine transitions: check current state, skip if already in target state
- Side effects gated by state: only execute on actual state change, not on every call
PATCH { quantity: +1 }: calling twice = +2. PATCH { quantity: 5 }: calling twice = still 5. Prefer absolute values over relative increments wherever possible. When increments are required, use an idempotency key to prevent double-application.
Rule 4: Conditional Updates with ETags
The rule: 'Use ETags and If-Match headers for optimistic concurrency control. Server: returns ETag: "v3" with the response (a version hash of the resource). Client: sends If-Match: "v3" with the update request. Server: checks if the current version matches "v3". If match: apply the update, return the new ETag ("v4"). If no match: return 412 Precondition Failed (someone else updated the resource since you read it). The client fetches the latest version and retries.'
For preventing lost updates: 'Two users edit the same document. User A reads version v3, User B reads version v3. User A saves: If-Match: v3 succeeds, resource is now v4. User B saves: If-Match: v3 fails (resource is now v4, not v3). User B is notified: "Someone else made changes. Review and retry." Without ETags: User B save overwrites User A changes silently (last-write-wins). With ETags: User B is informed of the conflict and can make an informed decision.'
AI generates: PUT /api/documents/123 with no version check. User A and User B both edit. User A saves. User B saves 5 seconds later: User A changes are silently overwritten. No conflict detection, no notification, no way to know changes were lost. ETags: the conflict is detected at save time. User B sees "Document was modified. Your version: v3, Current: v4. Review changes and retry." Lost updates become detected conflicts.
Rule 5: Idempotency Key Lifecycle Management
The rule: 'Idempotency keys have a lifecycle: (1) Created: client generates key, sends with request. (2) Pending: server receives key, starts executing (stored with status: pending). (3) Completed: operation succeeds, result stored with status: completed. (4) Failed: operation fails, error stored with status: failed (failed keys should be retryable — a new request with the same key re-executes if the stored status is failed). (5) Expired: key TTL reached (24 hours), key deleted. The pending state prevents concurrent execution of the same key.'
For concurrent request handling: 'Two identical requests arrive simultaneously (double-click, network retry before timeout). Request 1: SETNX idempotency:{key} status:pending — succeeds, starts executing. Request 2: SETNX idempotency:{key} — fails (key already exists, status: pending). Request 2 waits (poll every 100ms) or returns 409 Conflict ("Request is being processed, please wait"). When Request 1 completes: updates status to completed with the result. Request 2 poll: finds completed status, returns the stored result. Both requests get the same result. The operation executed once.'
AI generates: no handling of concurrent duplicate requests. Double-click the submit button: two POST requests arrive within 50ms. Both execute. Two orders created, two charges. With idempotency key + pending state: the first request claims the key, the second waits for the result. One order, one charge. The pending state is the mutex that prevents concurrent duplicate execution.
Double-click submit: two POST requests arrive within 50ms. Request 1 claims the key (status: pending), starts executing. Request 2 finds key exists, waits. Request 1 completes: stores result. Request 2 returns stored result. One order, one charge. The pending state is the mutex.
Complete Idempotency Rules Template
Consolidated rules for idempotency.
- Idempotency-Key header on all mutating POST requests — UUID per operation, same on retry
- Redis deduplication: SETNX key result EX 86400 — sub-ms lookup, auto-cleanup
- Database deduplication: INSERT ON CONFLICT for durability and transactional consistency
- Natural idempotency: PUT/DELETE inherently safe, PATCH with absolute values safe
- State machine transitions: check current state, skip side effects if already transitioned
- ETags + If-Match: prevent lost updates from concurrent edits — 412 on conflict
- Key lifecycle: pending → completed/failed → expired — pending prevents concurrent execution
- 24-hour TTL on keys: retries safe within window, new operations after expiry