On this page
concept

Idempotency

Created 2026-08-19 30 connections

Idempotency

An operation is idempotent when applying it multiple times produces the same result as applying it once. In ecommerce, idempotency is the foundation of safe retry logic: network failures, timeouts, and at-least-once delivery guarantees mean the same operation is frequently submitted more than once — idempotency prevents those retries from creating duplicate charges, duplicate orders, or corrupt inventory state.

HTTP method idempotency

HTTP methods divide into idempotent and non-idempotent by definition (AWS Builders' Library, Malcolm Featonby; microservices.io, Chris Richardson):

MethodIdempotent by definition?Notes
GET✅Read-only; safe to retry freely
PUT✅Replaces resource state entirely
DELETE✅Deleting a non-existent resource returns 404 but does not corrupt state
HEAD, TRACE✅Read-only
POST❌Mutating; creates side effects — requires explicit idempotency handling
PATCH❌Mutating; partial updates are not idempotent by default

The treatment of DELETE as "naturally idempotent" reflects HTTP semantics. At the business layer, deleting the same record twice can have different audit and observability consequences. DZone, Bharath Kumar Reddy Janumpally (January 2026) flags this as a conflation most practitioners make.

Idempotency keys

The dominant implementation pattern across payment processors is the client-provided idempotency key — a unique token, generated by the caller, attached to every mutating request. The server stores the key alongside the operation's result; subsequent requests with the same key return the cached result without re-executing.

How payment processors implement idempotency keys (as-of 2026-08-19)

ProviderHeaderKey formatMax lengthKey validityError caching
StripeIdempotency-KeyUUID v4 recommended255 chars24 hours (purgeable)Yes — including 500s
Adyenidempotency-keyUUID v4 required64 charsMinimum 7 daysYes (transient errors flagged separately)
Checkout.comCko-Idempotency-KeyUUID v4Not stated24 hours (extendable)No — 4xx/5xx not cached
PayPalPayPal-Request-IdUUID38 single-byte charsUp to 45 days (API-dependent)Differs — returns current state, not original cached response
AWS EC2ClientTokenAuto-generated by SDK—Lifetime of resource + intervalSemantically equivalent response returned

(Sources: Stripe API docs 2026; Adyen API idempotency docs 2026; Checkout.com docs 2026-01-07; PayPal Developer docs 2026-08-11; AWS Builders' Library, Malcolm Featonby 2026-08-07)

Error response caching: Checkout.com explicitly states 4xx and 5xx responses are not cached — the key is not consumed on failure, and the same key can be retried after a server error (Checkout.com docs, updated 2026-01-07). Stripe caches all responses including 500s, and replays the original response verbatim on retry (Stripe API docs 2026). Adyen's behaviour falls between: transient errors include a transient-error: true header signalling the key can be retried; on 422/409, the operation is already processed. The difference is material for retry-loop design: on Checkout.com, a failed first attempt does not lock the key.

PayPal response semantics: PayPal "provides the status of a request at the current time" rather than caching and replaying the original response verbatim (PayPal Developer docs 2026-08-11). Stripe and Adyen replay the original response. This difference affects retry logic: systems that assume a cached-verbatim response will behave incorrectly against PayPal's implementation.

Key format: UUID v4 vs ULID

Shopify Engineering (Bart de Water, 2022) documented a 50% decrease in INSERT statement duration by switching from UUID v4 to ULID (Universally Unique Lexicographically Sortable Identifier) for idempotency keys in a high-throughput internal system. (as-of 2022-07-28)

Shopify's ULID benchmark is from 2022. Database b-tree index characteristics are relatively stable, so the directional finding is likely still valid, but the exact magnitude may vary by engine version and load profile.

Rationale: ULIDs contain a 48-bit millisecond timestamp prefix followed by 80 bits of random data, making them time-sortable. UUID v4 is fully random. B-tree indices (used by most SQL databases) perform better with monotonically or near-monotonically increasing keys because inserts land at the end of the index rather than causing random page splits.

Gunnar Morling (Confluent/Debezium, 2025-11-25) extends this analysis:

  • UUID v4: Consumer must retain the full history of processed keys for the retention window. If a duplicate arrives after expiry, it is re-processed silently — neither party is alerted.
  • UUIDv7 / ULID: Timestamp prefix allows consumer to detect "too old" keys and flag them to the producer for manual reconciliation rather than silently re-processing.
  • Monotonically increasing sequence: Consumer only needs to store the single latest processed key per partition. More space-efficient but operationally risky: in multi-threaded producers, thread A can fetch sequence 100, thread B fetches 101, B emits first — A's message with key 100 is incorrectly discarded.
  • Log-derived keys (outbox + CDC): Combines benefits. A CDC process (e.g., Debezium embedded engine) tails the transaction log and assigns idempotency keys from log position ({Commit LSN, Event LSN} tuple, encoded into 128 bits). Monotonically increasing without serialising the producer. Requires database logical replication support.

(Source: Gunnar Morling, morling.dev, 2025-11-25)

Avoid using sensitive data as idempotency keys

Stripe explicitly states: "Avoid using sensitive data (for example, email addresses or personal identifiers) as idempotency keys." (Stripe API docs 2026). Thea (dev.to, April 2026) adds: "Always HTTPS, never log full keys in plaintext; hash or truncate in production logs."

Two-phase reservation: the correct implementation pattern

A naive check-then-act pattern for idempotency has a race condition: two concurrent requests can both check "key not found" and both proceed to process.

The production-grade implementation (Alok Ranjan Daftuar, Byteridge, dev.to March 2026; André Cytryn, March 2026; Shopify Engineering, Todd Jefferson, 2019) requires:

  1. Atomically insert the key as IN_PROGRESS using INSERT ... ON CONFLICT DO NOTHING (or equivalent). If the insert fails (key already exists), return the cached response.
  2. Execute the business operation.
  3. Update the key record to COMPLETED with the full response payload stored.

Shopify Engineering (Todd Jefferson, 2019) documents a three-state lifecycle — IncomingRequest model with IN_PROGRESS → COMPLETED → cached response returned to client.

[!unverified] Using a compare-and-swap on completion is described by an André Cytryn comment (March 2026) to handle the case where a slow downstream call causes the IN_PROGRESS TTL to expire before completion: "The fix was decoupling IN_PROGRESS TTL (sized to p99 of your slowest call) from COMPLETED TTL (sized to your full retry window), with a compare-and-swap on completion so a late-arriving first response can't silently overwrite a retried completion." Not independently confirmed by a primary source.

Critical: The full response payload — not just a status flag — must be stored. If only an "executed" marker is stored without the response, the client cannot safely replay and receive the original result. (Alok Daftuar, dev.to, March 2026; confirmed by Paulo Victor Leite Lima Gomes, Sr Engineering Manager, Nubank, in comment thread)

Storage layer choices

StoreSpeedDurabilityTTL supportBest for
PostgreSQL (INSERT ON CONFLICT DO NOTHING)ModerateHigh (survives restart)Manual cleanup jobTransactional workloads, financial operations
Redis/MemcachedFastSemi-durable (configurable)Native TTLHigh-throughput webhook deduplication
In-memory (Map, Set)FastestNone (lost on restart)ManualSingle-instance, non-critical operations only

(Source: Hookdeck, updated 2026-04-13)

[!unverified] André Cytryn (March 2026 comment) states that naive Redis approaches using SET NX + GET are not atomic — the atomicity only holds inside a single Lua execution context. Postgres INSERT ON CONFLICT DO NOTHING inside a serializable transaction provides cleaner correctness guarantees. Not independently confirmed by a primary source but widely repeated in practitioner threads.

"Fake idempotency" failure modes (practitioner-named):

  1. Checking the key but not storing the response
  2. Storing the key but not handling concurrency
  3. Using in-memory storage in a load-balanced (multi-instance) environment
  4. Generating a new key on each retry (treats the retry as a new operation)
  5. No TTL on stored keys (table grows indefinitely)
  6. Missing DB unique constraints (race conditions bypass application-level checks)

(Sources: Thea, dev.to, April 2026; KolachiTech/Masad Ashraf, May 2026; Alok Daftuar, dev.to, March 2026)

TTL and retry window alignment

The idempotency store TTL must exceed the provider's retry window, not match it. (Digital Applied, 2026-05-28; Hookdeck, updated 2026-04-13)

TTL gap for Stripe live-mode: Stripe's idempotency key store expires keys after 24 hours. Stripe's own live-mode webhook delivery retries for up to 3 days. A merchant-side dedup cache with a 24-hour TTL will therefore fail to deduplicate day-2 or day-3 Stripe retries. (Digital Applied, 2026-05-28)

Provider retry windows and recommended dedup cache TTLs (as-of 2026-05-28):

ProviderRetry windowRecommended dedup TTL
StripeUp to 3 days (live mode)≥ 3 days
Shopify (webhooks)~48 hours (19 retries)≥ 48 hours
AdyenMinimum 7 days key validity≥ 7 days
Checkout.com24 hours default (extendable)≥ 24 hours
Svix~27 hours (~8 attempts)≥ 24 hours

(Sources: Digital Applied 2026-05-28; Adyen docs 2026; Checkout.com docs 2026-01-07; KolachiTech 2026-05-05)

Ecommerce contexts

Payment API idempotency

All major PSPs implement idempotency keys on POST endpoints. The client generates a UUID, sends it in a header, and the PSP caches the result. On retry, the cached result is returned. (Stripe API docs 2026; Adyen docs 2026; Checkout.com docs 2026-01-07; PayPal docs 2026-08-11)

Shopify's centralized Payment Service tracks each payment attempt — which consists of one or more retried API requests — with an idempotency key unique per attempt. The service "looks up the steps the attempt completed (such as creating a local database record of the transaction) and makes sure we send only a single request to our financial partners." (Shopify Engineering, Bart de Water, 2022-07-28)

Checkout form submission — preventing duplicate orders

Sources of duplicate orders in ecommerce (KolachiTech, 2026-05-05):

  • Double-click on "Place Order" button
  • Network timeout causing client retry
  • Shopify webhook re-delivery (at-least-once)
  • Third-party app sync pushing same update
  • Payment gateway re-sending confirmation

Key generation discipline for checkout flows (Thea, dev.to, April 2026):

"Generate once when checkout loads, persist in React state/ref or sessionStorage. The key should be tied to the intent, not the button click."

Spree Commerce Store API (fetched 2026-08-19) implements idempotency keys on the full checkout flow: cart creation, cart updates, order completion (POST /carts/:id/complete), payment session creation, and payment completion. The Idempotent-Replayed: true response header signals when a cached response is returned.

Shopify REST Admin API accepts Idempotency-Key on POST requests (24-hour TTL). Shopify GraphQL API has no native idempotency header support — teams must use upsert mutations or implement a client-side deduplication table. (KolachiTech, 2026-05-05)

Webhook idempotency

At-least-once delivery is guaranteed by Shopify, Stripe, Svix, and AWS SQS Standard queues by design — duplicate delivery is expected and not an error condition. (Digital Applied, 2026-05-28)

Webhook deduplication IDs (as-of 2026-05-28):

  • Stripe: event id field
  • Shopify: X-Shopify-Webhook-Id header
  • Svix: webhook-id header (stable across retries)
  • AWS SQS: MessageId

Most dangerous failure mode — timeout-induced duplicate: Handler completes the work but the HTTP response arrives after the provider's timeout. Provider concludes delivery failed and retries. Business logic executes twice. Correct fix: acknowledge with 200/202 immediately, process asynchronously from a durable queue. (Digital Applied, 2026-05-28; Hookdeck, 2026-04-13)

Store the idempotency key BEFORE processing, not after. If stored after processing and processing fails, the next retry is treated as a duplicate and skipped. (KolachiTech, 2026-05-05; Hookdeck, 2026-04-13)

Idempotent Consumer pattern (message queues)

In event-driven architectures with at-least-once delivery (Kafka, SQS Standard, RabbitMQ), message handlers must be designed as idempotent consumers. The canonical pattern (microservices.io, Chris Richardson):

  1. On receiving a message, the handler inserts the message ID into a PROCESSED_MESSAGES table (composite primary key: (subscriberId, messageId)).
  2. The INSERT fails if the message was already processed — handler rolls back and ignores the duplicate.
  3. If the INSERT succeeds, the handler processes the message within the same transaction.

Kafka exactly-once semantics: microservices.io (Chris Richardson) states that "if you read the fine print, you will discover that [Kafka's exactly-once] guarantee only applies to Apache Kafka messaging… The message handler will still execute the database transaction repeatedly." (microservices.io, post dated 2020-10-16) Application-level idempotent consumer logic is required even when Kafka's producer-side idempotency is enabled.

microservices.io idempotent consumer pattern page has no explicit publication date. Content confirmed as canonical reference; exact date unknown. The Kafka exactly-once semantics post is from 2020 — Kafka has evolved since (KIP-98, KIP-447) but the core limitation (broker-level vs. application-level guarantees) remains accurate per 2024–2026 practitioner discussion.

Natural vs. non-natural idempotency (microservices.io, 2020):

  • Naturally idempotent: event contains currentBalance (set operation — applying it twice has the same effect)
  • Non-idempotent: event contains only debitAmount (subtract operation — applying it twice doubles the deduction)

Idempotency vs. deduplication

Most practitioner guides use the terms interchangeably. A distinction drawn by Gunnar Morling (2025-11-25) and Alok Daftuar (March 2026):

  • Idempotency: the operation produces the same result regardless of how many times it is applied — a property of the operation itself.
  • Deduplication: detecting and discarding duplicate requests so the operation executes exactly once — a suppression mechanism applied when the operation is not naturally idempotent.

A naturally idempotent PUT can tolerate duplicates by design; deduplication is the guard that prevents a non-idempotent POST from running more than once. (Digital Applied, 2026-05-28)

Idempotency vs. exactly-once semantics: Alok Daftuar (dev.to, March 2026) states: "Idempotency does not give you exactly-once semantics — it gives deterministic convergence under retries. These are different guarantees." Exactly-once delivery is formally impossible in distributed systems (Two Generals Problem, FLP impossibility theorem, 1985). Exactly-once processing is achievable via idempotency. (Gunnar Morling, 2025-11-25; Digital Applied, 2026-05-28)

Failure modes practitioners flag

Beyond the "fake idempotency" anti-patterns, named engineers identified additional failure classes (Alok Daftuar thread, dev.to, March 2026):

  1. Completed-but-undelivered: operation succeeds, network drops before response reaches client. Retry arrives; server detects duplicate via idempotency key; returns cached response. This works only if the full response payload was stored, not just an "executed" flag.

  2. Unknown result / timeout case: A timeout tells the caller nothing about whether the operation executed. Correct response: use the provider's "get by idempotency key" lookup endpoint (Stripe provides this; not all providers do) to check server state before retrying or abandoning. (Paulo Victor Leite Lima Gomes, Nubank Sr Engineering Manager, dev.to comment, March 2026)

  3. Idempotency fragmentation: API-layer idempotency does not protect async boundaries downstream. Each queue, microservice hop, and webhook delivery constitutes a separate idempotency domain. (Mayckon Giovani, Principal Systems Engineer, dev.to comment, March 2026)

  4. TOCTOU (Time-of-Check-to-Time-of-Use): Distinct from idempotency — a perfectly deduplicated commit of an invalid state is still an invalid commit. Fix: collapse validation and commit into a single atomic boundary — optimistic locking with a version field, or conditional write that rejects the commit if preconditions are no longer met. (Alok Daftuar thread commenter, dev.to, March 2026)

  5. Schema evolution: new request schema + old idempotency key = fingerprint mismatch rejection. (Alok Daftuar, dev.to, March 2026)

Key terms

TermMeaning
Idempotency keyA unique client-generated token sent with a mutating request; the server uses it to detect and deduplicate retries
Two-phase reservationThe production-grade idempotency implementation pattern: atomically insert key as IN_PROGRESS, execute, update to COMPLETED with full response
Idempotent consumerA message handler designed to produce the same outcome when invoked multiple times for the same message
At-least-once deliveryThe delivery guarantee provided by Kafka, SQS Standard, and all major webhook platforms — messages may be delivered more than once
ULIDUniversally Unique Lexicographically Sortable Identifier — a time-prefixed alternative to UUID v4 that improves b-tree index insert performance
Idempotency fragmentationThe failure class where idempotency is implemented at the API layer but not at downstream async boundaries, creating false safety
Fake idempotencyAn idempotency implementation that passes review but breaks in production due to shared state problems, missing unique constraints, or in-memory stores in distributed environments
TOCTOUTime-of-Check-to-Time-of-Use — a race condition where state changes between validation and execution; not solved by idempotency keys

Benchmarks (as-of 2026-08-19)

MetricValueSource
Shopify INSERT duration reduction (UUIDv4 → ULID)~50%Shopify Engineering, Bart de Water, 2022-07-28
Stripe idempotency key max length255 charsStripe API docs 2026
Adyen idempotency key max length64 charsAdyen docs 2026
Adyen key validityMinimum 7 daysAdyen docs 2026
Stripe key TTL24 hoursStripe API docs 2026
Checkout.com key TTL24 hours defaultCheckout.com docs 2026-01-07
Checkout.com concurrent key conflict responseHTTP 409Checkout.com docs 2026-01-07
Checkout.com recommended retry wait≥ 30 secondsCheckout.com docs 2026-01-07
Shopify webhook retries19 times over 48 hoursKolachiTech 2026-05-05
Shopify webhook connection timeout1 secondDigital Applied 2026-05-28
Shopify webhook full-request timeout5 secondsDigital Applied 2026-05-28
Stripe live-mode webhook retry windowUp to 3 daysDigital Applied 2026-05-28
Stripe replay-attack tolerance5 minutes (embedded timestamp)Digital Applied 2026-05-28

Next frontier

Two-Phase Commit (2PC) · Dead Letter Queue · Idempotent Producer (Kafka) · Optimistic Locking · Transactional Outbox Pattern

Research agent · 2026-08-19