Idempotency Patterns
Idempotency means: "Making the same request multiple times has the same effect as making it once."
Idempotency means: "Making the same request multiple times has the same effect as making it once."
Why Idempotency Matters
Networks Fail
Networks are unreliable. Retries are necessary. But retries without idempotency cause problems.
Common scenarios:
- User clicks "Submit" button, thinks it didn't work, clicks again
- Load balancer retries on backend timeout
- Client-side retry after network error
- Server-side retry after crash recovery
Without idempotency: Double charges, double account creation, double data modification.
Strategy 1: Idempotency Key
The client generates a unique key for each request. The server stores the key and result. On retry, the server checks the key first.
How it works:
- Client generates unique idempotency key (UUID, v4)
- Client sends key along with request
- Server checks if key exists in idempotency store
- If exists: return cached result
- If not: process request, store key+result, return result
Storage: Database table, Redis, dedicated idempotency service.
Key design considerations:
- Key lifetime: How long to keep idempotency records? 24 hours? 30 days? Depends on use case.
- Key format: UUID v4 is standard. Include user ID or request type for easier lookup.
- Concurrency: Handle concurrent requests with same key gracefully.
- Cleanup: Expire old records to prevent unbounded growth.
Use when: API requests where retries are likely, especially payment operations, account creation.
Example: Payment processing
- Client:
POST /payments { amount: 100, idempotency_key: "uuid-v4" } - Server: Stores key+result. On retry: returns cached result "payment successful"
- Retry count: Safe to retry multiple times
Strategy 2: HTTP Semantics
HTTP methods have built-in idempotency semantics:
- GET: Always idempotent. Should never have side effects.
- HEAD: Always idempotent. Same as GET but without body.
- PUT: Idempotent. Same request multiple times produces same result.
- DELETE: Idempotent. Same request multiple times removes resource once.
- POST: NOT necessarily idempotent. Can have side effects each time.
Key insight: Design your API to leverage HTTP semantics. Use PUT/DELETE for operations where idempotency is desired. Use POST cautiously.
Caution: Many APIs use POST for everything, losing built-in idempotency. Restructure to use appropriate HTTP methods.
Use when: Designing or evaluating APIs. Leverage HTTP standards where possible.
Strategy 2: Database Transactions with Idempotency Keys
Combine database transactions with idempotency keys for strong consistency.
How it works:
- Client sends request with idempotency key
- Server begins database transaction
- Check if key exists in idempotency table within same transaction
- If exists: rollback, return cached result
- If not: process request, insert key+result into idempotency table, commit transaction, return result
Pros: Strong consistency, atomic operation Cons: More complex, requires database support, idempotency store growth
Use when: Strong consistency required, already using database for other purposes, transaction-based operations.
Example: Bank transfer
- Begin transaction
- Check idempotency key for this transfer amount+from+to
- If exists: return cached result, rollback transaction
- If not: process transfer, insert key, commit transaction
Strategy 3: Event-Driven Idempotency
Combine idempotency with event-driven architecture.
How it works:
- Event producer sends event with idempotency key
- Event consumer checks key before processing
- If processed: skip, return acknowledgment
- If not processed: process event, store key, emit acknowledgment
Pros: Works well with event-driven architectures, natural retry handling Cons: Requires event infrastructure, additional storage for keys
Use when: Event-driven architecture already in place, event sourcing or CQRS patterns.
Strategy 3: Idempotent Service Design
Design services so operations are inherently idempotent.
How it works:
- Design operations so result depends only on current state, not request history
- Example: Increment a counter.
increment(counter)is idempotent if result = current_value + 1 regardless of how many times called. - Example: Toggle a boolean.
toggle(state)is idempotent: flips state each time, but result is always a valid state.
Not idempotent: charge_card(amount). Calling twice charges twice.
Use when: Designing new services, can influence API design.
Example: Feature flag toggle
POST /feature-toggles/{feature} { enabled: bool }- Result: toggles state. Calling twice flips back and forth, but state is always valid.
- Actually, this isn't idempotent either. Better example:
PUT /feature-toggles/{feature} { state: "enabled" }- if idempotent, multiple puts should produce same state state.
Strategy 4: Retry with Backoff and Dead Letter Queue
Combine retries with proper backoff and dead letter queue for non-idempotent operations.
How it works:
- Attempt operation
- On failure, wait exponential backoff
- Retry up to max attempts
- If still failing, send to dead letter queue for manual review
Key: Distinguish between transient failures (network, timeout) and permanent failures (validation, business rule).
Backoff strategy:
- Initial retry: immediate or 1 second
- Second retry: 2-4 seconds
- Third retry: 4-8 seconds
- Fourth retry: 8-16 seconds
- And so on, with max total retry time
Dead letter queue: Holds failed requests for manual review. Important for compliance and debugging.
Use when: Operations aren't naturally idempotent, must retry anyway.
The Honest Truth
Idempotency is invisible when it works and painfully obvious when it doesn't. Design for idempotency from the start for operations where retries are likely. For existing operations, add idempotency keys before you need themābefore the first customer complaint about double charges.
Published June 1, 2026 Ā· 10 min read