Idempotent Operations: Safe Retries in Distributed Systems
After this topic, you will be able to:
- Define idempotency and explain why it’s critical for retry safety in distributed systems
- Implement idempotency keys for API operations to prevent duplicate processing
- Apply idempotent design patterns: natural idempotency, deduplication tables, and versioning
- Design idempotent consumers for message queues with at-least-once delivery
TL;DR
Idempotent operations produce the same result when executed multiple times, making them safe to retry. Critical for distributed systems where network failures, timeouts, and at-least-once message delivery guarantee duplicate requests. Implement using idempotency keys, natural idempotency (PUT over POST), or conditional updates with versioning.
Cheat Sheet: Idempotency = f(x) = f(f(x)). Use client-generated idempotency keys stored in deduplication tables with TTL. Essential for payment processing, order placement, and any operation where duplicates cause harm.
The Problem It Solves
Picture this: A user clicks “Pay Now” on Uber, their network hiccups, the request times out, and they frantically click again. Without idempotency, they’d be charged twice. This isn’t theoretical—it happens thousands of times per day at scale.
Distributed systems face three fundamental challenges that make idempotency non-negotiable. First, network failures are inevitable. TCP guarantees delivery but not exactly-once semantics. A request might succeed on the server but the acknowledgment gets lost, causing the client to retry. Second, message queues typically provide at-least-once delivery (see Message Queues for delivery guarantees). Systems like SQS, RabbitMQ, and Kafka prioritize availability over exactly-once processing, meaning your consumer will see duplicate messages. Third, automatic retry logic in load balancers, API gateways, and client libraries means a single user action can trigger multiple server-side executions.
The consequences of non-idempotent operations are severe: duplicate charges, double inventory deductions, repeated notifications, and corrupted state. Stripe processes billions in payments annually—a single duplicate charge creates customer support nightmares, chargeback fees, and regulatory scrutiny. The problem compounds in microservices architectures where a single user request fans out to dozens of services, each with its own retry logic and failure modes.
Solution Overview
Idempotency makes operations safe to retry by ensuring that executing the same operation multiple times produces the same result as executing it once. Mathematically, an operation f is idempotent if f(x) = f(f(x)) = f(f(f(x))).
The solution has three layers. Natural idempotency leverages operations that are inherently safe to repeat—HTTP PUT and DELETE are idempotent by design, while POST is not. Setting a user’s email to “user@example.com” is idempotent; incrementing a counter is not. Idempotency keys provide explicit deduplication by having clients generate unique identifiers for each logical operation. The server stores these keys and rejects duplicates. Conditional updates use versioning or compare-and-swap semantics to ensure updates only apply once, even if the request is retried.
The key insight: idempotency is a contract between client and server. The client promises to use the same idempotency key for retries of the same logical operation. The server promises to detect duplicates and return the original result. This transforms an unreliable network into a reliable abstraction where retries are safe.
How It Works
Let’s walk through how Uber implements idempotent payment processing, a system handling millions of transactions daily where duplicate charges are unacceptable.
Step 1: Client generates idempotency key. When a user completes a ride, the mobile app generates a UUID v4: idem_key_7f3d9a2b-4c8e-4f1a-9b2d-6e5c8a1f3d9b. This key is tied to the specific ride and payment attempt. If the request fails and the app retries, it uses the same key. If the user manually retries, a new key is generated because it’s a new logical operation.
Step 2: Server receives request with idempotency key. The payment service receives POST /payments with the key in the Idempotency-Key header. Before processing, it queries the deduplication table: SELECT status, result FROM idempotency_keys WHERE key = 'idem_key_...' AND created_at > NOW() - INTERVAL '24 hours'. Keys expire after 24 hours to prevent unbounded storage growth.
Step 3: Handle duplicate detection. If the key exists and status is completed, return the cached result immediately—no payment processing occurs. If status is processing, another request with the same key is in-flight (race condition). The server either waits for the first request to complete or returns HTTP 409 Conflict. If the key doesn’t exist, proceed to step 4.
Step 4: Process operation with key insertion. The server inserts the key with status processing in a transaction: INSERT INTO idempotency_keys (key, status, created_at) VALUES (?, 'processing', NOW()). This uses a unique constraint to prevent concurrent duplicates. Then it processes the payment—charging the card, updating the ride record, and triggering notifications. If processing succeeds, it updates: UPDATE idempotency_keys SET status = 'completed', result = ? WHERE key = ?. The result (payment confirmation) is serialized and stored.
Step 5: Return response. The client receives the payment confirmation. If this response is lost and the client retries with the same key, step 3 returns the cached result. The payment only happened once.
Handling failures: If payment processing fails (card declined, timeout), the server updates status to failed and stores the error. Retries with the same key return the same error without re-attempting payment. If the server crashes mid-processing, the key remains in processing state. A background job detects stale processing keys (older than 5 minutes) and marks them as expired, allowing retry.
This pattern extends to message queues. When consuming from SQS, each message includes a deduplication ID. Before processing, check if that ID exists in your deduplication table. Process only if it’s new, then mark it as processed. This handles at-least-once delivery gracefully (see Message Queues for delivery semantics).
Idempotent Payment Processing Flow with Duplicate Detection
sequenceDiagram
participant Client
participant API as Payment API
participant Cache as Deduplication Store
participant DB as Payment DB
participant Card as Card Processor
Note over Client,Card: First Request (Network Timeout)
Client->>API: 1. POST /payments<br/>Idempotency-Key: idem_7f3d9a2b
API->>Cache: 2. Check key exists?
Cache-->>API: Not found
API->>Cache: 3. INSERT key='idem_7f3d9a2b'<br/>status='processing'
API->>Card: 4. Charge card $25.00
Card-->>API: Success (charge_id: ch_123)
API->>DB: 5. Save payment record
API->>Cache: 6. UPDATE status='completed'<br/>result='{charge_id: ch_123}'
API--xClient: Response lost (timeout)
Note over Client,Card: Retry Request (Same Key)
Client->>API: 7. POST /payments (RETRY)<br/>Idempotency-Key: idem_7f3d9a2b
API->>Cache: 8. Check key exists?
Cache-->>API: Found: status='completed'<br/>result='{charge_id: ch_123}'
API-->>Client: 9. Return cached result<br/>(No duplicate charge)
Shows how idempotency keys prevent duplicate payment processing. The first request completes but the response is lost. When the client retries with the same key, the server detects the duplicate and returns the cached result without recharging the card.
Concurrent Request Handling with Database Constraints
sequenceDiagram
participant C1 as Client 1
participant C2 as Client 2
participant API as API Server
participant DB as PostgreSQL
Note over C1,DB: Race Condition: Same Key, Simultaneous Requests
par Client 1 Request
C1->>API: POST /payments<br/>Key: idem_abc123
API->>DB: 1. INSERT INTO idempotency_keys<br/>(key, status) VALUES<br/>('idem_abc123', 'processing')
DB-->>API: Success (row inserted)
API->>API: 2. Process payment $50
API->>DB: 3. UPDATE status='completed'<br/>result='{...}'
API-->>C1: 200 OK {charge_id: ch_456}
and Client 2 Request (5ms later)
C2->>API: POST /payments<br/>Key: idem_abc123
API->>DB: 1. INSERT INTO idempotency_keys<br/>(key, status) VALUES<br/>('idem_abc123', 'processing')
DB-->>API: ❌ Unique constraint violation<br/>(key already exists)
API->>DB: 4. SELECT status, result<br/>WHERE key='idem_abc123'
Note over API,DB: Poll until status != 'processing'
DB-->>API: status='completed'<br/>result='{charge_id: ch_456}'
API-->>C2: 200 OK {charge_id: ch_456}<br/>(cached result)
end
Note over C1,DB: Only one payment processed, both clients get same result
Demonstrates how database unique constraints prevent concurrent duplicate processing. When two requests with the same idempotency key arrive simultaneously, the first succeeds and processes the operation. The second gets a constraint violation, waits for the first to complete, then returns the cached result.
Idempotency Key Implementation
Implementing idempotency keys correctly requires careful attention to key generation, storage, concurrency, and lifecycle management.
Key Generation (Client-Side): Clients must generate globally unique, unpredictable keys. Use UUID v4 or a combination of user ID, operation type, and timestamp: user_123_payment_1634567890123. Never use sequential IDs or predictable patterns—an attacker could guess keys and replay operations. The key must be deterministic for retries of the same logical operation but unique across different operations. Stripe’s approach: clients generate UUIDs and include them in the Idempotency-Key header. If a client doesn’t provide a key, Stripe generates one server-side based on request parameters, but this is less reliable.
Storage Schema: The deduplication table needs efficient lookups and automatic expiration. Example schema:
CREATE TABLE idempotency_keys (
key VARCHAR(255) PRIMARY KEY,
status ENUM('processing', 'completed', 'failed') NOT NULL,
result TEXT,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
INDEX idx_created_at (created_at)
);
The primary key on key ensures uniqueness and fast lookups. The created_at index supports TTL queries. Store the serialized result (JSON) to return cached responses. For high-throughput systems, partition by key prefix or use a distributed cache like Redis with TTL: SET idem_key_... {status, result} EX 86400.
Lookup and Validation: On each request, perform a point lookup: SELECT status, result FROM idempotency_keys WHERE key = ? AND created_at > ?. The created_at filter implements TTL—keys older than 24 hours are ignored. If the key exists, validate that the request parameters match the original. Airbnb’s booking system stores a hash of request parameters alongside the key. If a client retries with the same key but different dates, it’s rejected as invalid.
Handling Concurrent Requests: Two requests with the same key arriving simultaneously create a race condition. Use database constraints to prevent duplicates: INSERT INTO idempotency_keys ... ON DUPLICATE KEY UPDATE ... (MySQL) or INSERT ... ON CONFLICT DO NOTHING (PostgreSQL). The first request wins and proceeds; the second gets a unique constraint violation and either waits or returns 409 Conflict. For distributed systems, use distributed locks (Redis SETNX) or optimistic locking with version numbers.
TTL and Cleanup: Keys must expire to prevent unbounded growth. Implement TTL at the database level (partition pruning, TTL indexes) or application level (background job deleting old keys). Stripe expires keys after 24 hours. Longer TTLs increase storage costs; shorter TTLs risk rejecting legitimate retries after network partitions. Balance based on your retry window and storage constraints.
Idempotency Key Lifecycle and Storage Pattern
stateDiagram-v2
[*] --> Generated: Client generates UUID
Generated --> Lookup: Server receives request
Lookup --> NotFound: Key not in store
Lookup --> Processing: Key exists, status='processing'
Lookup --> Completed: Key exists, status='completed'
Lookup --> Failed: Key exists, status='failed'
Lookup --> Expired: Key exists but TTL exceeded
NotFound --> Insert: INSERT with status='processing'
Insert --> Execute: Process operation
Execute --> UpdateSuccess: Operation succeeds
Execute --> UpdateFailed: Operation fails
UpdateSuccess --> Completed: UPDATE status='completed'<br/>Store result
UpdateFailed --> Failed: UPDATE status='failed'<br/>Store error
Processing --> Wait: Concurrent request detected
Wait --> Completed: First request completes
Wait --> Failed: First request fails
Completed --> ReturnCached: Return stored result
Failed --> ReturnError: Return stored error
Expired --> NotFound: Treat as new request
ReturnCached --> [*]
ReturnError --> [*]
Completed --> Cleanup: TTL expires (24h)
Failed --> Cleanup: TTL expires (24h)
Cleanup --> [*]: DELETE from store
State machine showing the complete lifecycle of an idempotency key from generation through expiration. Keys transition through processing, completed, or failed states, with TTL-based cleanup preventing unbounded storage growth.
Variants
Natural Idempotency (Design-Level): Some operations are inherently idempotent without explicit keys. HTTP PUT is idempotent—PUT /users/123 {email: 'new@example.com'} sets the email regardless of how many times it’s called. HTTP DELETE is idempotent—deleting a non-existent resource succeeds. Use natural idempotency when possible; it’s simpler and has no storage overhead. When to use: Read operations, absolute updates (set to value), deletes. Pros: No infrastructure required, zero storage cost. Cons: Doesn’t work for relative updates (increment), creates, or operations with side effects (sending emails).
Deduplication Tables (Explicit Keys): The pattern described above—clients provide keys, servers store them in a database. When to use: Financial transactions, order placement, any operation where duplicates cause harm. Pros: Works for any operation, provides audit trail, supports complex retry logic. Cons: Requires storage, adds latency (extra database query), needs TTL management.
Conditional Updates (Optimistic Locking): Use version numbers or timestamps to ensure updates only apply once. Example: UPDATE orders SET status = 'shipped', version = version + 1 WHERE id = 123 AND version = 5. If the version changed (another request updated it), the update fails. When to use: Collaborative editing, inventory management, state machines. Pros: No separate deduplication table, prevents lost updates. Cons: Requires version field in every table, clients must handle version conflicts.
Distributed Deduplication (Bloom Filters): For high-throughput systems, use probabilistic data structures. A Bloom filter can check if a key was seen before with low memory overhead. When to use: Stream processing with millions of events per second. Pros: Constant memory usage, extremely fast lookups. Cons: False positives possible (may reject valid requests), no result caching, requires fallback to exact deduplication for critical operations.
Idempotency Variants: Natural vs Explicit vs Conditional
graph TB
subgraph Natural Idempotency
A1["PUT /users/123<br/>{email: 'new@example.com'}"] --> A2["Set email = 'new@example.com'"]
A2 --> A3["Result: Email is 'new@example.com'"]
A3 -."Retry".-> A2
A2 -."Same result".-> A3
end
subgraph Explicit Keys - Deduplication Table
B1["POST /payments<br/>Idempotency-Key: uuid_123"] --> B2["Check dedup table"]
B2 --> B3{Key exists?}
B3 -->|No| B4["INSERT key<br/>Process payment"]
B3 -->|Yes| B5["Return cached result"]
B4 --> B6["Store result with key"]
end
subgraph Conditional Updates - Optimistic Locking
C1["UPDATE orders<br/>SET status='shipped'<br/>version=version+1"] --> C2["WHERE id=123<br/>AND version=5"]
C2 --> C3{Version matches?}
C3 -->|Yes| C4["Update succeeds<br/>version now 6"]
C3 -->|No| C5["Update fails<br/>version changed"]
C5 -."Retry with new version".-> C1
end
Note1["Use for: Absolute updates,<br/>reads, deletes"] -.-> Natural Idempotency
Note2["Use for: Payments, creates,<br/>side effects"] -.-> Explicit Keys - Deduplication Table
Note3["Use for: State machines,<br/>collaborative editing"] -.-> Conditional Updates - Optimistic Locking
Comparison of three idempotency approaches. Natural idempotency requires no infrastructure but only works for certain operations. Explicit keys work universally but require storage. Conditional updates prevent lost updates using version numbers.
Trade-offs
Storage vs. Safety: Idempotency keys require persistent storage—every operation generates a database row or cache entry. At Uber’s scale (millions of rides daily), this is terabytes of deduplication data. Trade-off: Store keys for 24 hours (balances retry window and storage cost) or 7 days (handles extended outages but increases storage 7x). Decision criteria: If your retry window is under 1 hour, 24-hour TTL suffices. For systems with manual retries or long-running operations, extend to 7 days.
Latency vs. Correctness: Every request requires a deduplication lookup before processing—an extra database query adds 5-10ms. Trade-off: Skip deduplication for read-only operations (faster but not idempotent) or apply universally (slower but safe). Decision criteria: Apply idempotency only to mutating operations (POST, PUT, DELETE). For reads, use caching instead.
Client Complexity vs. Server Complexity: Who generates idempotency keys? Client-generated keys (Stripe’s approach) push complexity to clients—every SDK must implement key generation. Server-generated keys (based on request hash) simplify clients but are less reliable (hash collisions, parameter ordering issues). Trade-off: Client-generated keys are more robust but require client updates. Server-generated keys work with any client but may miss duplicates. Decision criteria: For public APIs, require client-generated keys. For internal services, server-generated keys reduce coordination.
Synchronous vs. Asynchronous Deduplication: Check for duplicates before processing (synchronous) or after (asynchronous with rollback). Synchronous adds latency but prevents wasted work. Asynchronous is faster but may process duplicates and need compensation. Trade-off: Synchronous deduplication adds 10ms but prevents duplicate charges. Asynchronous deduplication is faster but requires idempotent side effects and rollback logic. Decision criteria: For financial operations, always use synchronous. For analytics or logging, asynchronous is acceptable.
When to Use (and When Not To)
Use idempotent operations when:
Financial transactions: Payments, refunds, transfers—duplicates cause direct monetary loss and regulatory issues. Stripe, PayPal, and Square all require idempotency keys for payment APIs.
Inventory management: Decrementing stock, reserving items—duplicates cause overselling. Amazon’s order placement is idempotent; retrying doesn’t create multiple orders.
State transitions: Moving an order from pending to shipped—duplicates corrupt state machines. Use conditional updates with version numbers.
External side effects: Sending emails, SMS, push notifications—duplicates annoy users. Twilio’s API uses idempotency keys to prevent duplicate messages.
At-least-once message processing: When consuming from SQS, Kafka, or RabbitMQ, duplicates are guaranteed. Make consumers idempotent (see Task Queues for retry patterns).
Avoid idempotency when:
Analytics and logging: Duplicate log entries are acceptable; the overhead isn’t worth it. Use sampling or approximate counting instead.
Idempotent by nature: Read operations, absolute updates (set email), deletes—these are naturally idempotent. Don’t add explicit keys.
Low-stakes operations: Incrementing a view counter, updating last-seen timestamp—duplicates have minimal impact. Optimize for throughput instead.
Real-time systems with tight latency budgets: If you can’t afford the 5-10ms deduplication lookup, use eventual consistency and accept occasional duplicates. Design downstream systems to handle them.
Real-World Examples
Stripe (Payment Processing): Stripe’s API requires idempotency keys for all mutating operations. When creating a charge, clients include Idempotency-Key: unique_key_123 in the header. Stripe stores keys in a distributed cache (Redis) with 24-hour TTL. If a request with an existing key arrives, Stripe returns the cached response without re-processing. Interesting detail: Stripe validates that retry requests have identical parameters to the original. If you retry with the same key but different amount, it returns HTTP 400. This prevents accidental misuse where a client reuses keys across different operations. Stripe processes over $640 billion annually—idempotency prevents millions in duplicate charges.
Uber (Ride Completion and Payment): When a ride ends, Uber’s mobile app generates an idempotency key tied to the trip ID and payment attempt. The payment service stores keys in PostgreSQL with a 24-hour TTL. If the app crashes or network fails during payment, retrying with the same key returns the original result. Uber extends idempotency to the entire ride completion workflow—updating trip status, charging the rider, paying the driver, and sending receipts. All operations use the same idempotency key, ensuring the entire workflow is atomic and retriable. Interesting detail: Uber’s deduplication table is partitioned by date for efficient TTL cleanup. A daily cron job drops partitions older than 7 days, avoiding expensive DELETE operations.
Airbnb (Booking System): Airbnb’s booking flow is idempotent end-to-end. When a guest books a property, the client generates an idempotency key. The booking service checks availability, reserves the dates, charges the guest, and notifies the host—all tied to that key. If the request times out, retrying with the same key skips already-completed steps. Interesting detail: Airbnb stores a hash of booking parameters (dates, property ID, guest count) alongside the idempotency key. This prevents a malicious client from reusing a key to book different dates. The hash ensures retries are truly retries, not new operations masquerading as duplicates.
Stripe’s End-to-End Idempotent Payment Architecture
graph LR
Client["Mobile App"] -->|"1. POST /v1/charges<br/>Idempotency-Key: uuid_789<br/>amount=2500"| Gateway["API Gateway"]
Gateway -->|"2. Route request"| PaymentAPI["Payment Service"]
PaymentAPI -->|"3. Check key"| Redis[("Redis Cache<br/><i>24h TTL</i>")]
Redis -.->|"Key exists?<br/>Return cached"| PaymentAPI
PaymentAPI -->|"4. Validate params hash"| ParamStore[("Parameter Store")]
ParamStore -.->|"Hash mismatch?<br/>Return 400"| PaymentAPI
PaymentAPI -->|"5. Store key + hash<br/>status='processing'"| Redis
PaymentAPI -->|"6. Charge card"| CardProcessor["Card Processor<br/><i>Stripe Connect</i>"]
CardProcessor -.->|"Success/Failure"| PaymentAPI
PaymentAPI -->|"7. Update key<br/>status='completed'<br/>result='{...}'"| Redis
PaymentAPI -->|"8. Persist charge"| PostgreSQL[("PostgreSQL<br/><i>Charges Table</i>")]
PaymentAPI -->|"9. Return response"| Gateway
Gateway -->|"200 OK<br/>{charge_id: ch_xyz}"| Client
Redis -.->|"Daily cleanup job<br/>DELETE keys > 24h"| Cleanup["Cleanup Service"]
Stripe’s production idempotency implementation processing $640B annually. Uses Redis for fast key lookups with 24-hour TTL, stores parameter hashes to prevent key reuse across different operations, and implements daily cleanup jobs to manage storage growth.
Interview Essentials
Mid-Level
Define idempotency and explain why it’s necessary in distributed systems. Describe the idempotency key pattern: client generates UUID, server stores it in a deduplication table, duplicate requests return cached results. Implement a simple idempotent API endpoint with key lookup and storage. Explain natural idempotency (PUT vs POST) and when to use each. Discuss TTL for idempotency keys and why it’s needed. Be ready to write SQL for the deduplication table schema.
Senior
Design an idempotent payment processing system handling 10,000 requests/second. Discuss trade-offs between database-backed deduplication (durable but slower) and cache-backed (faster but risk of data loss). Explain how to handle concurrent requests with the same idempotency key using database constraints or distributed locks. Describe conditional updates with versioning for state machines. Discuss idempotency in message queues with at-least-once delivery—how do you make consumers idempotent? Explain the difference between idempotency (same result) and exactly-once processing (single execution). Be ready to calculate storage requirements: 1M requests/day, 24-hour TTL, 1KB per key = 24GB.
Staff+
Architect idempotency across a microservices ecosystem where a single user action fans out to 20 services. Discuss distributed idempotency—how do you ensure all services agree on whether an operation was processed? Explain saga patterns with idempotent compensating transactions. Design a system where idempotency keys are hierarchical (parent key for the entire workflow, child keys for each step). Discuss cross-region idempotency with eventual consistency—how do you handle a key existing in US-East but not US-West? Explain Bloom filters for probabilistic deduplication at scale. Discuss the CAP theorem implications: in a partition, do you reject requests (CP) or risk duplicates (AP)? Be ready to debate: should idempotency be enforced at the API gateway, service layer, or database layer?
Common Interview Questions
How do you implement idempotency for a payment API? Walk through the flow from client generating a key to server detecting duplicates.
What’s the difference between idempotency and exactly-once processing? (Idempotency allows multiple executions but ensures same result; exactly-once prevents multiple executions.)
How do you handle a client retrying with the same idempotency key but different request parameters? (Reject as invalid—keys must be tied to specific operations.)
What happens if two requests with the same idempotency key arrive simultaneously? (Use database unique constraints or distributed locks; first wins, second waits or fails.)
How long should you store idempotency keys? (Balance retry window and storage cost; 24 hours is common, 7 days for critical operations.)
Can you make a counter increment idempotent? (Not naturally, but you can use conditional updates: SET count = 5 WHERE count = 4.)
How does idempotency relate to message queue delivery guarantees? (At-least-once delivery requires idempotent consumers to handle duplicates.)
Red Flags to Avoid
Confusing idempotency with exactly-once processing—they’re related but distinct concepts.
Implementing idempotency only at the API layer but not in message consumers—duplicates will occur in queues.
Using sequential IDs or predictable patterns for idempotency keys—creates security vulnerabilities.
Not implementing TTL for idempotency keys—leads to unbounded storage growth.
Assuming network retries are rare—at scale, retries are constant and must be handled.
Making operations idempotent by ignoring duplicates—this hides bugs rather than solving them.
Not validating that retry requests match the original—allows key reuse across different operations.
Key Takeaways
Idempotency is mandatory for retry safety: In distributed systems with network failures and at-least-once delivery, operations must produce the same result when executed multiple times. Non-idempotent operations cause duplicate charges, corrupted state, and data inconsistencies.
Idempotency keys provide explicit deduplication: Clients generate unique keys (UUIDs) for each logical operation. Servers store keys in deduplication tables with TTL (typically 24 hours). Duplicate requests return cached results without re-processing. This pattern works for any operation, especially financial transactions and state changes.
Natural idempotency is simpler when possible: HTTP PUT and DELETE are inherently idempotent. Absolute updates (set email to value) are idempotent; relative updates (increment counter) are not. Use natural idempotency to avoid storage overhead, but fall back to explicit keys for creates and operations with side effects.
Concurrent duplicates require careful handling: Two requests with the same key arriving simultaneously create race conditions. Use database unique constraints (INSERT … ON CONFLICT) or distributed locks (Redis SETNX) to ensure only one processes. The first request wins; the second waits or returns 409 Conflict.
Idempotency extends beyond APIs to message queues: At-least-once delivery in SQS, Kafka, and RabbitMQ guarantees duplicates. Make consumers idempotent by checking deduplication tables before processing. This transforms unreliable message delivery into reliable workflows (see Message Queues and Task Queues for implementation patterns).