Retried requests  / field guide
Practitioner field guide · 2026-09-01

A timeout is not an answer: making retried requests safe

A client that times out on a mutating call holds no evidence about whether it happened, and the only safe recovery is to send it again. This guide reconstructs, from Stripe, Airbnb, Shopify, AWS, Kafka and three decades of RPC research, the machinery that makes the resend harmless: what the idempotency key actually promises, the five clauses of that contract, and the published incidents where one missing clause charged real credit cards up to fifty times.

30 primary sources 20+ production implementations 5 published incidents Evidence through September 2026 Read: ~25 min
01

The territory

The problem, stated without naming a technology: when a mutating request dies without a reply, resending is mandatory and re-executing is forbidden, and something has to absorb the difference.

42 yrs
Age of the mechanism: client-generated call identifiers for duplicate elimination, published 1984
50×
Times a single card was charged in the 2018 Coinbase duplicate-charge event, per press reports
<4%
Overhead of exactly-once RPC on a 13.5 µs write in RAMCloud's RIFL implementation
2
Kafka releases (3.0.0, 3.1.0) that shipped idempotence "on by default" while a validation bug left it off

On a night in July 2013, Twilio's billing system read every customer balance as zero, decided every account needed topping up, charged the stored credit card, failed to record that it had done so, and concluded from the failure that it should try again. It kept concluding that until on-call engineers shut the whole billing system down at 4:10 a.m. Pacific time. Twilio's postmortem reports that the auto-recharge service "billed the customers before updating their balance internally" and "continued to retry the transaction again and again, resulting in multiple charges to customer's credit cards." Nothing in that loop was exotic. Every part of it, a read, a charge, a write, a retry, is code most teams have shipped.

The underlying problem is older than every company in this guide and does not mention HTTP. A sender that gets no reply cannot distinguish "the request never arrived" from "it was applied and the reply was lost." Pat Helland's 2012 ACM Queue article Idempotence Is Not a Medical Condition states the consequence flatly: messages get lost and are retried after a timeout, so "every message may be retried and, hence, must be idempotent." The retry is not a design choice you can decline. It is a property of the substrate. The only real choice is whether the second arrival of the same intent is absorbed or executed.

The mechanism the industry converged on is a client-generated unique identifier for the intent, sent with the request and remembered by the server. Birrell and Nelson published it for RPC in 1984: a call identifier of machine id, process id and sequence number that "allows elimination of duplicate call packets." Amazon shipped it for EC2 instance launches as ClientToken in 2010. Stripe's Idempotency-Key header entered its public documentation around 2015, a dating that Hatchet's history hunt pinned down in 2026 after finding that most secondary accounts cite it earlier without primary sources. Kafka rebuilt it inside the broker in 2016 as producer ids plus per-partition sequence numbers, which KIP-98 describes as working "in a way similar to TCP." Different decades, different layers, one mechanism.

Figure 1 · Every hop can resend; each dedupe layer covers only the hop it spans

may resend

may resend

may resend

may resend

may repost

Person
double-click, impatient reload

Client application
retries on timeout

SDK / client library
auto-retry, backoff + jitter

Your API service
idempotency layer + key store

Payment provider API
its own Idempotency-Key

Card network / bank rails
reversal + repost cycles

may resend

may resend

may resend

may resend

may repost

Person
double-click, impatient reload

Client application
retries on timeout

SDK / client library
auto-retry, backoff + jitter

Your API service
idempotency layer + key store

Payment provider API
its own Idempotency-Key

Card network / bank rails
reversal + repost cycles

The application's idempotency layer (marked) absorbs resends arriving from above it. The 2018 Coinbase duplicates were created two hops below it, inside the card network's reversal-and-repost cycle, where no application key could reach. Sources: Stripe, 2017; TechCrunch, 2018.
Diagram source

What makes the topic worth a field guide rather than a tutorial is the gap between how simple the idea sounds and how much of it is contract rather than code. The IETF draft that would standardise the header, draft-ietf-httpapi-idempotency-key-header, lists nine organisations using the Idempotency-Key header and eleven more implementing the same concept under other names: PayPal's PayPal-Request-Id, Square's body-level idempotency_key, OpenBanking's x-idempotency-key, Google's requestId. Twenty implementations, at least nine spellings, and still no RFC. Mark Nottingham's first attempt to standardise safe POST retry, POST Once Exactly, expired in September 2005; the current draft, which credits it, reached draft-07 in October 2025 and remains a draft. Two decades of standards effort have not caught up with a pattern every payments company reimplements privately, which tells you where the actual knowledge lives: in engineering blogs, incident reports and source code, not in a spec.

Figure 2 · Four decades of one mechanism, and the standard that never landed

1984 · Birrell and Nelson publish RPC call identifiers
for duplicate elimination (Xerox PARC)

2005 · POST Once Exactly draft (Nottingham)
expires unadopted

2010 · EC2 RunInstances gains ClientToken
for idempotent launches

2015 · Stripe's Idempotency-Key header in public docs;
RIFL brings exactly-once RPC to SOSP

2016 · Kafka KIP-98 puts producer ids and
sequence numbers inside the broker

2019 · Airbnb publishes Orpheus,
its payments idempotency framework

2021 · IETF HTTPAPI working group adopts
the Idempotency-Key draft

2025 · Draft-07 published in October.
Still not an RFC

1984 · Birrell and Nelson publish RPC call identifiers
for duplicate elimination (Xerox PARC)

2005 · POST Once Exactly draft (Nottingham)
expires unadopted

2010 · EC2 RunInstances gains ClientToken
for idempotent launches

2015 · Stripe's Idempotency-Key header in public docs;
RIFL brings exactly-once RPC to SOSP

2016 · Kafka KIP-98 puts producer ids and
sequence numbers inside the broker

2019 · Airbnb publishes Orpheus,
its payments idempotency framework

2021 · IETF HTTPAPI working group adopts
the Idempotency-Key draft

2025 · Draft-07 published in October.
Still not an RFC

The mechanism keeps being rebuilt one layer up: RPC runtime, cloud control plane, payments API, message broker. The standardisation track (2005, 2021–2025) has expired or stalled each time. Sources: Birrell & Nelson 1984, AWS 2010, Hatchet 2026, IETF datatracker.
Diagram source
The finding that surprised us

The machinery that guarantees "at most once" is itself software, and it fails like software. Kafka's KIP-679 made idempotence the producer default in release 3.0.0. KAFKA-13598 then found that in 3.0.0 and 3.1.0 the config reported enable.idempotence=true while the validation logic never actually applied it. For two releases the strongest delivery guarantee was on in the docs and off on the wire, and nothing failed loudly, because the absence of deduplication is silent by construction. Every failure in section 04 shares that shape: the dedupe layer breaks quietly, and the symptom appears somewhere else, on someone's bank statement.

Scope. This guide covers request-level duplicate suppression: idempotency keys and client tokens at API boundaries, broker-level producer dedupe, and the store and lifecycle behind them. It deliberately does not cover keeping a database write and its outbound event in agreement (this practice's earlier dig on the outbox and change data capture covers that seam), saga-style business compensation, distributed transactions and two-phase commit, or the fencing and transaction internals of stream processors beyond what the dedupe layer needs.

02

How it is actually built

Across Stripe's published design, Airbnb's Orpheus, Shopify's payment service, AWS client tokens and RAMCloud's RIFL, the same five responsibilities appear. Companies differ on who drives recovery and what a reused key returns, not on the shape.

The clearest public account of the server side is Brandur Leach's 2017 article Implementing Stripe-like Idempotency Keys in Postgres, written while he worked at Stripe, and it starts from a distinction the tutorials skip: identify every foreign state mutation, meaning any effect outside your transactional store. His warning generalises beyond payment calls: "It's tempting to treat emitting records to Kafka as part of atomic operations because they have such a high success rate that they feel like they are. They're not, and should be treated like any other fallible foreign state mutation." Everything between two foreign mutations can be grouped into an atomic phase and committed together; the foreign mutations themselves cannot be rolled back, only recorded and resumed past. Airbnb's Orpheus framework is the same decomposition with different names: every request splits into Pre-RPC, RPC and Post-RPC phases, where the RPC is the foreign mutation. RIFL, the SOSP 2015 exactly-once RPC layer, factors the identical problem into four sub-problems: RPC identification, completion-record durability, retry rendezvous and garbage collection. Three vocabularies, one architecture.

Figure 3 · The reference shape: claim, verify, resume, replay

POST + Idempotency-Key

no

yes

finished

locked, in flight

resumable

foreign mutation, with its
own downstream key

Client: generate key,
persist it with the intent,
retry with the same key

Claim key atomically
(unique index or lock)

Request fingerprint
matches stored one?

Reject: validation error,
key reuse with new payload

Recorded state?

Replay stored response
(success or terminal error)

409: request outstanding

Run next atomic phase,
advance recovery point

External system

Key store: key, fingerprint,
recovery point, response

POST + Idempotency-Key

no

yes

finished

locked, in flight

resumable

foreign mutation, with its
own downstream key

Client: generate key,
persist it with the intent,
retry with the same key

Claim key atomically
(unique index or lock)

Request fingerprint
matches stored one?

Reject: validation error,
key reuse with new payload

Recorded state?

Replay stored response
(success or terminal error)

409: request outstanding

Run next atomic phase,
advance recovery point

External system

Key store: key, fingerprint,
recovery point, response

Reconstructed from Brandur Leach's Stripe-pattern implementation (2017), Airbnb's Orpheus (2019) and the IETF draft's enforcement rules. A parameter mismatch on a seen key returns a validation error (422 in the draft, a parameter-mismatch error at AWS) rather than entering the state machine.
Diagram source

The five clauses of the contract

1. Identity of intent. The client, not the server, names the operation, because only the client knows that two transmissions are one intent. Every implementation scopes the key to the caller: Brandur's schema makes the unique index (user_id, idempotency_key), and the IETF draft's security section recommends a composite lookup key for the same reason, so one tenant cannot collide with or probe another's keys. The client side of the clause is just as load-bearing and lives in the SDKs: stripe-ruby generates a key automatically the moment retries are enabled, with the comment "It is only safe to retry network failures on post and delete requests if we add an Idempotency-Key header," and GoCardless's client libraries do the same. A retry policy without a key is not a resilience feature; it is a duplicate generator with jitter.

2. Atomic claim. Two copies of the same request will race, so the first touch of the key must be a single atomic operation. Brandur's implementation wraps the upsert in a SERIALIZABLE transaction and lets Postgres abort one of two concurrent claimants; Shopify's payment service takes "a lock around the API call based on the client and idempotency key" and answers the loser with a 409, per its 2023 resilient-payments write-up. The IETF draft encodes the same split: a retry after completion gets the stored result, a retry while the original is still running gets a conflict.

3. Parameter binding. A key must be bound to the payload it first travelled with, or a buggy client that reuses keys across different requests silently gets wrong answers replayed. Stripe errors on reuse with different parameters; AWS returns a validation error "indicating a parameter mismatch between idempotent requests," per the Amazon Builders' Library; the IETF draft says the key "MUST NOT be reused with another request with a different request payload" and sketches the fingerprint mechanism, a server-side checksum of the payload. Notably, what the fingerprint is actually for is still being argued in the working group's own tracker (issue #36), fourteen years after Stripe shipped it.

4. Progress, not just presence. The naive implementation stores "seen" and gives you at-most-once: a crash after the foreign mutation but before the response is stored leaves a key that can neither complete nor replay. The production implementations all store where the request got to. Brandur's recovery points (started, ride_created, charge_created, finished) let a retry resume mid-lifecycle instead of restarting; Orpheus classifies every error as retryable or non-retryable and lets clients "repeatedly fire the same request" until the state machine drains, a strategy Airbnb calls write repair; RIFL's completion record is the same idea durably logged with the RPC's result. Who drives that recovery is a real divergence point: Airbnb leans on clients to keep firing, while Brandur's design adds a server-side completer because "there can be cases where a client starts working, never quite finishes, and drops forever." If your clients are third parties you do not control, the completer is not optional.

5. Replay, expiry, and the reaper. A finished key stores the entire response and replays it, and at Stripe that includes failures: a comment in stripe-ruby notes that the client does not bother retrying most 500s because "our idempotency framework would typically replay it anyway." Errors are results too; replaying them is what makes the outcome stable. The store is then garbage-collected, and the window is part of the public contract: Stripe prunes keys after 24 hours and states plainly that "we generate a new request if a key is reused after the original is pruned." Expiry is the clause everyone forgets: after the window, the same key is not a retry any more, it is a fresh command.

Where the store lives

Brandur: a table in the service's own Postgres, so claims share transactions with business writes. Airbnb: a dedicated idempotency database, sharded by idempotency key when one primary stopped scaling. Kafka: inside the broker, keyed by producer id and partition sequence. The store is on the hot path of every mutating request; Shopify measured a 50% INSERT-time reduction switching key format from UUIDv4 to ULID (2023).

Sources: brandur.org, Airbnb, Shopify

What the platform will not do for you

Sidekiq's own wiki is blunt: jobs run at least once, a completed job can re-run if Redis drops the acknowledgement, and "Sidekiq makes no exactly-once guarantee at all." Uniqueness is a paid Enterprise feature with an explicit time window. The job queue's refusal is the correct end-to-end reading: the platform can dedupe its own hop, never your handler's side effects.

Source: Sidekiq wiki

Keys compose downstream

Brandur's reference implementation passes its own derived key (rocket-rides-atomic-#{key.id}) to Stripe, so a crashed server plus a client retry still cannot double-charge: the downstream provider dedupes on its copy. Uber's platform docs describe the same assumption from the other side; external payment providers "are normally implementing their services as idempotent message processors." Each hop protects the hop below it by forwarding identity, not by hoping.

Sources: brandur.org, Uber, 2018

03

The decisions that matter

Four forks where published systems went different ways for stated reasons, and the condition that flips each one.

When a completed key is reused: replay the stored response, or refuse?

Replay (Stripe, Square, IETF draft)
  • Stripe and Square store the full response and return it verbatim on reuse; Stripe replays stored errors as well as successes.
  • The IETF draft makes replay the SHOULD for retries after completion.
Refuse (GoCardless)
  • GoCardless allows a key "for a single successful request" and answers any reuse with a 409 idempotent_creation_conflict, pointing at the already-created resource.
  • Simpler store: no response bodies retained.
Flips when
  • Replay wins when callers are third parties who may have lost the original response and cannot re-derive it.
  • Conflict wins for create-only endpoints where the caller can fetch the resource by the key, and it removes a whole class of stale-replay bugs.

Dedupe in the platform, or in the application?

Both, with eyes open
  • Kafka's KIP-98 dedupes the produce hop with sequence numbers, and KIP-679 judged the cost low enough to make default.
  • Application keys still guard the business effect end to end.
Platform-only dedupe
  • Sidekiq explicitly refuses to promise exactly-once execution.
  • CircleCI's December 2025 incident shows why: duplicate completion events met a consumer (auto-rerun) that was not idempotent, and duplicates cascaded anyway.
Flips when
  • Platform dedupe is sufficient only when the platform's hop is the entire side effect (a produced record, a stored row).
  • The moment the consumer does something non-idempotent with the delivery, the guarantee must move up into it.

How long does the promise last?

Published, bounded windows
  • DynamoDB transactions: 10 minutes. Stripe: prunes at 24 hours. Shopify: "typically 24 hours or less." Adyen: minimum 7 days.
Keys forever
  • Brandur: keys are "not meant to be used as a permanent request archive"; his reaper suggestion is 72 hours so a bad Friday deploy can still be repaired on Monday.
Flips when
  • The window must exceed the longest plausible retry: an automated backoff finishes in minutes, a customer-support replay or batch reconciliation arrives days later.
  • Stripe's own doc names the failure: a key reused after pruning starts a new request. Expiry turns a late retry back into a duplicate.

One transaction, or phases with recovery points?

Phases (Stripe pattern, Orpheus, RIFL)
  • Required the moment a foreign state mutation sits inside the request; the mutation cannot be rolled back, so progress must be recorded around it.
Whole-request transaction
  • Brandur's stated preference where it fits: for endpoints that only mutate local ACID state, map the request to one transaction and skip the key machinery entirely; he calls the phase design "far easier and less complicated" to avoid.
Flips when
  • Count the foreign mutations. Zero: one transaction. One or more: phases, with each mutation carrying its own downstream key.
  • And none of it works on a store without atomic multi-row commit; Brandur notes that without transactions every write is effectively foreign.

Figure 4 · Choosing the machinery an endpoint actually needs

yes

no

no

yes

yes

no

Can the operation be stated as
absolute state, e.g. PUT this value?

No key machinery.
Make the write naturally idempotent.

Does the request contain any
foreign state mutation?

Map the request to one
ACID transaction. Done.

Will clients reliably keep
retrying until finished?

Key + phases + replay.
Client-driven write repair.

Key + phases + replay,
plus a server-side completer
and a reaper with a published window.

yes

no

no

yes

yes

no

Can the operation be stated as
absolute state, e.g. PUT this value?

No key machinery.
Make the write naturally idempotent.

Does the request contain any
foreign state mutation?

Map the request to one
ACID transaction. Done.

Will clients reliably keep
retrying until finished?

Key + phases + replay.
Client-driven write repair.

Key + phases + replay,
plus a server-side completer
and a reaper with a published window.

Terminal nodes are actions, not "it depends." Derived from Brandur Leach's design rules (2017) and Airbnb's client write-repair assumption (2019).
Diagram source
DecisionChosenRejectedBecauseEvidence
Reused completed keyReplay stored response, errors includedRecomputeRecomputing re-executes side effects; replay is the definition of the guaranteeStripe docs, stripe-ruby
Reused key, new payloadValidation error (422 / mismatch)Serve the cached responseServing would hand caller B caller A's resultIETF draft, AWS Builders' Library
Concurrent duplicate409, caller retries shortlyBlock until first finishesBlocking ties up a worker for the length of a foreign callbrandur.org, Shopify
Claim concurrency controlSERIALIZABLE upsert (Brandur) or explicit lock + 409 (Shopify)Read-then-writeTwo racing claimants both pass the readbrandur.org
Key formatULIDUUIDv4Timestamp-prefixed keys keep the index hot at the right end: 50% faster INSERTs at ShopifyShopify, 2023
Who generates keysSDK auto-generates when retries are onTrust integrators to rememberThe unsafe combination (retry without key) should be unrepresentablestripe-ruby, GoCardless
Broker defaultIdempotence on (Kafka 3.0+)Opt-in (2017–2021)KIP-679's analysis found acks=all costs little; took a second KIP and four yearsKIP-679
04

What broke in production

Five published incidents and one confirmed contract break, in three classes: the retry loop with no dedupe, the dedupe machinery itself failing, and duplicates born outside the boundary your keys can reach.

The public record here is thin in an instructive way. Trawling the maintained postmortem collections, Twilio 2013 is the only classic write-up whose mechanism is a duplicate-side-effect retry loop; most duplicate-charge events surface as status-page notices and press coverage rather than engineering postmortems. Two readings are possible: the pattern mostly works, or duplicate side effects get classed as billing errors and settled by support teams without an engineering write-up. The Zuora and Coinbase entries below, both documented outside engineering blogs, point to the second reading. Treat the scarcity as a detection gap, not as evidence of safety.

Figure 5 · Twilio 2013: the anatomy of a keyless retry loop

Card gatewayBalance store (redis,read-only afterrestart)Auto-rechargeserviceCard gatewayBalance store (redis,read-only afterrestart)Auto-rechargeserviceattempt recorded as failed,charge already realloop[until engineers shut billing down, 4:10 a.m.]read balance0 (balances lost in failedrecovery)charge stored cardcharge succeededwrite updated balancewrite refused (read-only)charge the same card again
Card gatewayBalance store (redis,read-only afterrestart)Auto-rechargeserviceCard gatewayBalance store (redis,read-only afterrestart)Auto-rechargeserviceattempt recorded as failed,charge already realloop[until engineers shut billing down, 4:10 a.m.]read balance0 (balances lost in failedrecovery)charge stored cardcharge succeededwrite updated balancewrite refused (read-only)charge the same card again
The charge succeeds, the local record of it fails, and the failure is read as "try again." Every retry loop that wraps a foreign mutation and its local bookkeeping as one unit has this failure inside it. Reconstructed from Twilio's postmortem (2013).
Diagram source
Postmortem

Twilio, July 2013: charge first, record second, retry forever

AssumptionThe balance store would always accept the write that records a completed charge; recharge attempts were treated as retryable as a unit.
What happenedA network partition triggered mass redis resynchronisation; the restarted master came up read-only with balances lost. Auto-recharge "billed the customers before updating their balance internally," the balance write failed, and the system "continued to retry the transaction again and again."
Blast radiusRepeated charges to customer cards through the night until engineers shut billing down at 4:10 a.m. PDT; account suspensions from wrong balances.
FixFail-safes so that "if billing balances don't exist or cannot be written, the system will not suspend accounts or charge credit cards," plus an independent double-bookkeeping record used to restore balances.
Design ruleThe foreign mutation and the local record of it are different steps and must never share one retry decision. Record intent durably before charging; treat "charged but unrecorded" as its own state, never as failure.
SourceTwilio postmortem, 2013; independent analysis by antirez
Postmortem

Zuora, November 2024: the unknown response code becomes a duplicate charge

AssumptionThe gateway's response-code vocabulary was fully mapped; anything unrecognised could be handled as a failure.
What happenedThe Vantiv gateway introduced response code 136 on November 5; Zuora's incident notice reports the unmapped code led to processing "which may have resulted in duplicate charges," with response-code mapping as the fix.
Blast radiusMerchants on the legacy Vantiv gateway over roughly a week in November 2024, per the incident timeline.
FixMapping the new response code correctly.
Design ruleIn a payment path there are three outcomes, not two: success, failure, and unknown. Only failure is safely retryable. An unrecognised response code is unknown, and unknown must park the attempt for reconciliation, never feed the retry loop.
Source

Kafka 3.0/3.1, 2021–22: the default that read true and did nothing

AssumptionChanging a config default changes behaviour; KIP-679 was voted, documented and released in 3.0.0.
What happenedKAFKA-13598: "the validator and the idempotence enabled check method were not adjusted, so that if a user didn't explicitly enable idempotence, this feature wouldn't be turned on," in 3.0.0 and 3.1.0. A related fix (KAFKA-13673) had to disable idempotence when user configs conflict.
Blast radiusEvery producer relying on the documented default across two releases; unquantifiable, because missing dedupe produces no error, only occasional duplicates under retry.
Fix3.0.1, 3.1.1 and 3.2.0 apply the default properly. 3.2.0 then surfaced the next surprise: producers hitting older brokers without the IDEMPOTENT_WRITE ACL began failing, documented by a community PR to the upgrade notes.
Design ruleA delivery guarantee is not a config value; it is behaviour. Verify it behaviourally, by injecting a duplicate-producing fault in a test and asserting exactly one effect, after every upgrade of the layer that promises it.
Source

EC2, January 2025: the provider quietly edits a 14-year-old contract

AssumptionReplay semantics documented since 2010 were stable: rerunning RunInstances with the same ClientToken succeeds and returns the same instance.
What happenedboto3 issue #4406 (confirmed as a bug): calling RunInstances with the token of a since-terminated instance "used to succeed according to EC2 documentation, but the operation started failing with an IdempotentInstanceTerminated exception," breaking callers' automation.
Blast radiusAutomation that relied on replay-after-terminate; reported January 26, 2025.
FixTracked as a confirmed bug against the documented behaviour at the time of research; the useful fact is that the behaviour changed underneath users at all.
Design ruleAnother party's idempotency window and replay semantics are their implementation detail, not your invariant. Pin your logic to what the resource state says, and treat the provider's dedupe as an optimisation you re-verify, not a foundation.
Postmortem

Coinbase / Visa / Worldpay, February 2018: duplicates born below every application

AssumptionDeduplication at the merchant and processor layers covers the customer-visible outcome.
What happenedAfter a merchant category code change for crypto purchases, Visa "refunded and recharged transactions under a different merchant category"; reposts landed before refunds, so cardholders saw duplicate charges. The joint statement is explicit: "This issue was not caused by Coinbase."
Blast radiusPurchases between January 22 and February 11, 2018; press reports describe single cards charged 17 and even 50 times, with drained accounts and overdraft fees.
FixNetwork-side reversals and credits, coordinated across Worldpay, Visa and issuing banks over several days.
Design ruleYour key store bounds what you can prevent, not what your customer experiences. Budget for duplicates arriving from layers you do not operate: detection queries on settled transactions, a reversal runbook, and support tooling, even when your own pipeline is provably exactly-once.
Postmortem

CircleCI, December 2025: duplicate events meet a non-idempotent consumer

AssumptionWorkflow completion events fire once, so consumers may act on each one unconditionally.
What happenedA deploy "introduced a race condition in how workflows are terminated," and when concurrent jobs terminated, the system "published duplicate workflow completion events. For customers with auto-rerun enabled, these duplicate events triggered duplicate workflows to be created, causing cascading issues across the platform."
Blast radiusDuplicate workflows and duplicate notification emails from December 2 16:20 to December 3 14:20 UTC; API and UI degradation on December 3; fully resolved 22:50.
FixRollback of the code change, then re-enabling auto-rerun.
Design ruleEvery event consumer that triggers work is an API endpoint without a client to blame; give it the same key discipline, deduping on the event id or a derived intent key before acting. "Fires exactly once" is an assumption about someone else's race conditions.
05

Numbers you can plan against

What the dedupe layer costs, how long the promises last, and what the incidents measured, each figure with its context and date.

MetricValueAtContextAs ofSource
Exactly-once RPC overhead<4% on 13.5 µs writesRAMCloud (RIFL)Measured; durable completion records in a research system; distributed transactions ~20 µs2015SOSP paper
Key-insert cost reduction, ULID vs UUIDv4−50% INSERT durationShopifyMeasured; one high-throughput payment system's idempotency-key table2023Shopify Engineering
Dedupe window10 minutesDynamoDB TransactWriteItemsVendor-documented; same token after the window is a new requestcurrent docs, 2026AWS docs
Dedupe window24 hours (pruning eligible)StripeVendor-documented; "we generate a new request if a key is reused after the original is pruned"current docs, 2026Stripe API reference
Dedupe window≥7 daysAdyenVendor-documented minimum validity after first submissioncurrent docs, 2026Adyen docs
Broker idempotence default cost"not significant"Apache KafkaClaimed in KIP-679's analysis of acks=1 to acks=all; the KIP made it default in 3.02020KIP-679
Releases with the default silently off2 (3.0.0, 3.1.0)Apache KafkaReported; config validation bug, fixed in 3.0.1/3.1.1/3.2.02022KAFKA-13598
Duplicate-event exposure window~22 hoursCircleCIDerived: duplicate workflows/emails Dec 2 16:20 to Dec 3 14:20 per the incident report2025incident report
Max duplicate charges on one cardup to 50Coinbase customersReported in press; reversal-and-repost inside the card network2018TechCrunch
Implementations catalogued by the would-be standard20 orgs, ≥9 spellingsIETF HTTPAPI WGReported in the draft's implementation-status section2025-10draft-07
Recommended reaper horizon~72 hoursStripe pattern (Brandur)Stated rationale: survive a bad Friday deploy through to a Monday fix and completer replay2017brandur.org
Read these carefully

Measured: the RIFL and Shopify rows. Vendor-documented promises: the window rows, which are contracts that can change (the EC2 entry in section 04 is exactly such a change). Claimed: KIP-679's cost analysis, which the community accepted but which your workload should re-measure. Derived: the CircleCI window, computed from the report's own timestamps. Unknown, because nobody publishes it: production duplicate-arrival rates, key-store hit ratios, and the p99 cost of the key check on the request path. If you deploy this machinery, you will have to generate those numbers yourself; rung 7 below is designed to.

06

The evidence wall

Every source behind this page, graded. The full claim-by-claim ledger ships beside this file as sources.md. Filter by kind.

Postmortem Twilio2013-07

Billing Incident Post-Mortem: Breakdown, Analysis and Root Cause

The canonical duplicate-charge postmortem: read-only balance store, charge-then-record ordering, and a retry loop that kept charging until humans intervened at 4:10 a.m.

Carry forwardNever let the foreign mutation and its local record share one retry decision.
twilio.com
Postmortem CircleCI2025-12

Post Incident Report: December 2, 2025

A race condition published duplicate workflow-completion events; the auto-rerun consumer acted on each one, creating duplicate workflows and cascading load.

Carry forwardEvent consumers that trigger work need key discipline as much as API endpoints do.
status.circleci.com
Postmortem Zuora2024-11

Duplicate charges through the legacy Vantiv gateway

A gateway introduced response code 136; the unmapped code was handled as a retryable failure and "may have resulted in duplicate charges" until the mapping was fixed.

Carry forwardSuccess, failure, unknown: three outcomes. Only failure is retryable; unknown goes to reconciliation.
trust.zuora.com
Postmortem Coinbase / Visa / Worldpay2018-02

Joint statement on duplicate card transactions

"This issue was not caused by Coinbase": a merchant-category change made the card network reverse and repost transactions, and cardholders saw the reposts first.

Carry forwardDuplicates can be born in layers you do not operate; budget detection and reversal, not just prevention.
blog.coinbase.com
Source Stripecurrent

stripe-ruby: api_requestor.rb

The client half of the contract in production code: "It is only safe to retry network failures on post and delete requests if we add an Idempotency-Key header," plus the note that Stripe's idempotency framework replays stored 500s.

Carry forwardMake the unsafe combination unrepresentable: enabling retries auto-generates the key.
github.com/stripe/stripe-ruby
Source Apache Kafka2022-01

KAFKA-13598: idempotence not enabled by default despite the default

The config said true; the validator never applied it. Two releases of the strongest delivery guarantee existing only in documentation.

Carry forwardVerify delivery guarantees behaviourally after upgrades; their absence is silent.
issues.apache.org
Source AWS / boto3 users2025-01

boto3 #4406: RunInstances "not idempotent anymore. Even with client token"

Replay-after-terminate behaviour documented since 2010 changed underneath users; confirmed as a bug. The provider's dedupe contract moved.

Carry forwardA third party's idempotency semantics are their implementation detail; anchor on resource state.
github.com/boto/boto3
Source Sidekiqcurrent

Best Practices: "Sidekiq makes no exactly-once guarantee at all"

The job tier's deliberate refusal: completed jobs can re-run if the acknowledgement is lost, so handlers must be idempotent; uniqueness is a paid feature with a declared window.

Carry forwardThe platform dedupes its hop at best. The handler owns the side effect.
github.com/sidekiq wiki
Source IETF HTTPAPI WGopen

Issue #36: What value does the Idempotency Fingerprint add?

The payload-binding mechanism, shipped by Stripe since the mid-2010s, is still an open argument in the standards tracker, including an internal consistency question about the draft's own sections.

Carry forwardBind keys to payloads in your implementation regardless of where the standard lands.
github.com/ietf-wg-httpapi
ADR IETF HTTPAPI WG2025-10

The Idempotency-Key HTTP Header Field (draft-07)

The would-be standard, fetched in full: uniqueness and no-reuse-across-payloads as MUSTs, replay for completed retries, conflict for concurrent ones, and an implementation-status section listing twenty organisations.

Carry forwardUse its enforcement table as your endpoint's acceptance tests.
datatracker.ietf.org
ADR IETF (Nottingham)2005, expired

POST Once Exactly

The first standardisation attempt: a resource that answers POST successfully exactly once. Expired September 2005; the current draft explicitly credits it.

Carry forwardTwenty years of failed standardisation means portability between providers is on you.
datatracker.ietf.org
ADR Apache Kafka2016 / 2020

KIP-98 and KIP-679

KIP-98 designs broker-side dedupe as producer id plus per-partition sequence numbers, "similar to TCP," and weighs the message-format overhead. KIP-679 makes it the default four years later after judging the cost negligible.

Carry forwardSequence numbers per sender-partition are the cheapest dedupe when the transport owns ordering.
cwiki.apache.org
Blog Stripe (Brandur Leach)2017-10

Implementing Stripe-like Idempotency Keys in Postgres

The most complete public server-side design: foreign state mutations, atomic phases, recovery points, the completer and the reaper, with a runnable reference implementation.

Carry forwardCount your foreign mutations first; the architecture falls out of that count.
brandur.org
Blog Airbnb (Jon Chew)2019-04

Avoiding double payments in a distributed payments system

Orpheus: Pre-RPC / RPC / Post-RPC phases, retryable versus non-retryable error classification, client-driven write repair, and a dedupe database sharded by idempotency key when one primary stopped scaling.

Carry forwardClassify every error path as retryable or not at design time; ambiguity here is where doubles come from.
medium.com/airbnb-engineering
Blog Shopify (Bart de Water)2023-05

10 Tips for Building Resilient Payment Systems

The operational numbers nobody else publishes: ULID keys halving INSERT time, windows of 24 hours or less, and lock-plus-409 on concurrent reuse.

Carry forwardThe key store is hot-path infrastructure; measure it like one.
shopify.engineering
Blog Stripe2017-02

Designing robust and predictable APIs with idempotency

The client contract from the provider's mouth: generate a unique ID per operation, retry with the same ID, back off with jitter.

Carry forwardPublish the client rules with the API; half the mechanism runs on machines you do not own.
stripe.com/blog
Blog GoCardless2017

Safely retrying API requests

The deliberate dissenter: one successful use per key, then 409 idempotent_creation_conflict, with client libraries auto-generating keys.

Carry forwardConflict-not-replay is a legitimate simpler contract for create-only endpoints.
gocardless.com
Blog Uber2018 / 2020

Payments platform and money movement at scale

Design goals stated as "exactly-once payment processing by the means of idempotency and strong consistency," implemented through immutable orders persisted before processing and a versioned entity change log, with double-entry bookkeeping as the audit layer that catches what dedupe misses.

Carry forwardPair idempotency with an independent ledger; prevention and detection are separate controls.
uber.com/blog
Blog Hatchet2026

The First Idempotency Key

A primary-source hunt through the pattern's history: Stripe's header dates to ~2015 in the documented record, EC2's ClientToken precedes it, and the trail ends at PARC-era RPC research.

Carry forwardWhen secondary sources agree on a date without citations, distrust the date.
hatchet.run
Paper Helland (ACM Queue)2012-04

Idempotence Is Not a Medical Condition

The argument from first principles that retries are a property of loosely coupled systems, so every message must tolerate re-arrival.

Carry forwardDesign the handler for the second arrival first; the first arrival is the easy case.
queue.acm.org
Paper Stanford (Lee et al.)2015-10

Implementing Linearizability at Large Scale and Low Latency (RIFL)

Exactly-once as a reusable infrastructure layer: RPC identification, durable completion records, retry rendezvous, garbage collection; under 4% overhead on microsecond-scale writes.

Carry forwardThe four sub-problems are your implementation checklist, whatever the stack.
stanford.edu
Paper Birrell & Nelson (Xerox PARC)1984

Implementing Remote Procedure Calls

The origin: call identifiers of machine id, process and sequence number that allow "elimination of duplicate call packets" so a procedure is not executed twice when only the acknowledgement was lost.

Carry forwardThe idempotency key is not a payments invention; it is RPC's call id at a new layer.
web.eecs.umich.edu
Talk Confluent (Gustafson)2019-04

Exactly Once Semantics Revisited (Kafka Summit NYC)

Two years after shipping, the implementer's accounting of "remaining gaps" in exactly-once and how they were being addressed; slides published alongside.

Carry forwardEven the flagship implementation needed a revisit; plan for your dedupe layer to have a v2.
confluent.io
Talk Confluent (Sax)2018-04

Don't Repeat Yourself: Introducing Exactly-Once Semantics in Apache Kafka

The mechanism decomposed for practitioners: idempotent producer, transactions and fencing as three separable pieces rather than one magic property.

Carry forward"Exactly-once" is always a bundle; ask which pieces a vendor's claim actually includes.
confluent.io
Case study TechCrunch2018-02

Visa confirms Coinbase wasn't at fault for overcharging users

Independent reporting quantifying the blast radius of network-level duplicates: cards charged 17 and 50 times, drained accounts, overdraft fees.

Carry forwardThe customer's measure of your idempotency is their bank statement, not your architecture.
techcrunch.com
Vendor AWS (Featonby)2021-01

Making retries safe with idempotent APIs (Builders' Library)

Amazon's institutional pattern: client tokens on mutating APIs, parameter-mismatch validation errors, and the reasoning a principal engineer gives internal teams.

Carry forwardThe token-plus-mismatch-error pair is the minimum viable server contract.
aws.amazon.com
Vendor Stripe / Adyen / AWScurrent

The window fine print: 24 hours, 7 days, 10 minutes

Three current vendor documents defining how long the same key means "the same request": Stripe prunes at 24 hours, Adyen holds at least 7 days, DynamoDB transactions 10 minutes. Same word, three orders of magnitude.

Carry forwardRead the window before you design the retry policy that depends on it.
docs.stripe.com
Vendor Squarecurrent

Idempotency (developer docs)

The same contract transported in the request body rather than a header, with replay of the stored success and an error on reuse with changed parameters.

Carry forwardTransport varies freely; the semantics are the standard that never got written down.
developer.squareup.com
07

Build a miniature, then productionise it

Seven rungs from an evening's middleware to the operational surface. The toy-to-real line is crossed at rung 4, where you start killing the process on purpose.

Dedupe one POST endpoint in memory

A toy API with one mutating endpoint; a map from key to stored response; reject requests missing the header.

Done when: two identical curls return byte-identical responses and the side effect count is 1.  Teaches: replay is the guarantee, not "we noticed the duplicate."

Move the store to a database, bind the payload

Unique index on (caller, key); store a payload fingerprint and the response; return a validation error on fingerprint mismatch and 409 while the first attempt holds the lock, per the IETF draft's enforcement table.

Done when: two parallel requests with one key produce exactly one 2xx and one 409, under a loop of 1,000 runs.  Teaches: the atomic claim, and why read-then-write cannot provide it.

Put a foreign mutation inside

Add a fake payment provider with its own key support and nonzero latency. Split the endpoint into atomic phases with recovery points, forwarding a derived key downstream, following the Stripe pattern.

Done when: the state machine's phases match Figure 3 and every phase commits before the next foreign call.  Teaches: Brandur's rule that the foreign mutation is the unit the design bends around.

Kill the process mid-request, then retry

kill -9 between the provider call and the response write; retry with the same key and watch it resume from the recovery point instead of double-charging.

Done when: 100 randomized kill points yield zero duplicate charges and zero stuck requests.  Teaches: progress-not-presence; the naive "seen" flag fails this rung.

Add the completer and the reaper

A background process that finds unfinished keys whose clients gave up and drives them to completion; a reaper that prunes finished keys past the window. Then write the test that reuses a key after pruning and observe the duplicate it creates.

Done when: the expiry-duplicate test passes only when the client's maximum retry horizon is shorter than the window.  Teaches: the window is a contract clause, and Stripe's docs state its failure mode outright.

Inject the published failures

Reproduce Zuora's bug (make the provider return an unknown response code; assert it parks the attempt instead of retrying) and Twilio's (make the local record store read-only after the charge; assert the loop halts and alarms).

Done when: both faults produce a paged alert and zero additional provider calls.  Teaches: the three-outcome model, and that the dedupe layer needs its own failure detection.

Measure the numbers nobody publishes

Load-test the key check's p99 on the request path, the store's growth rate against the reaper, and duplicate-arrival rate under injected client retries; compare ULID and UUIDv4 key formats against Shopify's 50% figure. Add a reconciliation query that counts provider-side charges per intent, the control Uber's double-entry design and Twilio's independent record both point at.

Done when: a dashboard shows duplicate-absorption rate, key-store p99 and reconciliation drift, and an on-call runbook says what to do when each moves.  Teaches: day-two ownership; prevention plus detection, never one alone.

08

Keep hunting

The queries that found this material, grouped by what they surface. The domain vocabulary that unlocks the good layer: "double charge," "client token," "exactly-once," "at-least-once," "write repair," "response code mapping."

Incidents and postmortems

  • "idempotency" payments postmortem "double charge" incident retry
  • status page incident "duplicate charges" OR "duplicate webhooks" retry root cause
  • billing incident post-mortem redis "charged multiple times"
  • site:github.com danluu post-mortems billing duplicate

Production designs

  • "idempotency keys" postgres implementing stripe-like brandur
  • airbnb "avoiding double payments" idempotency orpheus
  • shopify engineering "resilient payment systems" idempotency ULID
  • uber engineering payments idempotency "exactly once" money movement

The argument layer: ADRs, issues, rejected drafts

  • IETF idempotency-key header draft httpapi working group
  • "POST once exactly" Nottingham draft expired
  • KIP-679 idempotence default kafka "acks=all"
  • KAFKA-13598 enable.idempotence default not applied
  • boto3 issue RunInstances "not idempotent anymore" client token

The mechanism's lineage

  • Birrell Nelson "implementing remote procedure calls" 1984 duplicate call
  • RIFL "implementing linearizability at large scale" SOSP exactly-once RPC
  • Helland "idempotence is not a medical condition"
  • EC2 RunInstances client token idempotency window docs
09

References

  1. Brandur Leach, Implementing Stripe-like Idempotency Keys in Postgres brandur.org, 2017-10-27. Fetched in full via the author's public source mirror. Checked 2026-09-01.
  2. Stripe (Brandur Leach), Designing robust and predictable APIs with idempotency stripe.com/blog, 2017-02. Checked 2026-09-01.
  3. Stripe API reference: Idempotent requests docs.stripe.com, current. Checked 2026-09-01.
  4. stripe-ruby, lib/stripe/api_requestor.rb github.com/stripe/stripe-ruby, master. Fetched in full. Checked 2026-09-01.
  5. Jon Chew, Avoiding double payments in a distributed payments system Airbnb Engineering, Medium, 2019-04. Checked 2026-09-01.
  6. Bart de Water, 10 Tips for Building Resilient Payment Systems Shopify Engineering, 2023-05. Checked 2026-09-01.
  7. GoCardless, Safely retrying API requests gocardless.com/blog, 2017. Checked 2026-09-01.
  8. Uber Engineering, Engineering Uber's Next-Gen Payments Platform uber.com/blog, 2018. Checked 2026-09-01.
  9. Uber Engineering, Revolutionizing Money Movements at Scale with Strong Data Consistency uber.com/blog, 2020. Checked 2026-09-01.
  10. Gergely Orosz, Distributed architecture concepts I learned while building a large payments system pragmaticengineer.com, 2018. Checked 2026-09-01.
  11. Twilio, Billing Incident Post-Mortem: Breakdown, Analysis and Root Cause twilio.com, 2013-07. Checked 2026-09-01.
  12. Salvatore Sanfilippo, Twilio incident and Redis antirez.com, 2013-07. Checked 2026-09-01.
  13. CircleCI, Post Incident Report: December 2, 2025 status.circleci.com, 2025-12. Checked 2026-09-01.
  14. Zuora, incident: payment processing through the legacy Vantiv gateway trust.zuora.com, 2024-11. Checked 2026-09-01.
  15. Visa and Worldpay, Joint Statement for Coinbase customers blog.coinbase.com, 2018-02. Checked 2026-09-01.
  16. TechCrunch, Visa confirms Coinbase wasn't at fault for overcharging users techcrunch.com, 2018-02-16. Checked 2026-09-01.
  17. IETF HTTPAPI WG (Jena, Dalal), The Idempotency-Key HTTP Header Field datatracker.ietf.org, draft-07, 2025-10-15. Draft source fetched in full from the WG repository. Checked 2026-09-01.
  18. Mark Nottingham, POST Once Exactly (POE) datatracker.ietf.org, 2005-03, expired 2005-09-17. Checked 2026-09-01.
  19. IETF HTTPAPI WG, issue #36: What value does Idempotency Fingerprint add? github.com/ietf-wg-httpapi/idempotency. Checked 2026-09-01.
  20. Pat Helland, Idempotence Is Not a Medical Condition ACM Queue 10(4), 2012-04. Checked 2026-09-01.
  21. Lee, Park, Ousterhout et al., Implementing Linearizability at Large Scale and Low Latency SOSP 2015. Checked 2026-09-01.
  22. Birrell and Nelson, Implementing Remote Procedure Calls ACM TOCS 2(1), 1984. Checked 2026-09-01.
  23. Apache Kafka, KIP-98: Exactly Once Delivery and Transactional Messaging cwiki.apache.org, 2016-11. Checked 2026-09-01.
  24. Apache Kafka, KIP-679: Producer will enable the strongest delivery guarantee by default cwiki.apache.org, 2020-10. Checked 2026-09-01.
  25. Apache Kafka JIRA, KAFKA-13598: idempotence producer is not enabled by default if not set explicitly issues.apache.org, 2022-01, fixed 3.0.1/3.1.1/3.2.0. Checked 2026-09-01.
  26. apache/kafka PR #12260: add note on IDEMPOTENT_WRITE ACL to 3.2.0 notable changes github.com/apache/kafka, merged 2022-06. Checked 2026-09-01.
  27. Factor House, Apache Kafka 3.2.0: Idempotent Producer Breaking Change factorhouse.io, 2022. Checked 2026-09-01.
  28. Neha Narkhede, Exactly-once Semantics is Possible: Here's How Apache Kafka Does it confluent.io/blog, 2017-06. Checked 2026-09-01.
  29. Jason Gustafson, Exactly Once Semantics Revisited Kafka Summit NYC, 2019-04. Video and slides. Checked 2026-09-01.
  30. Matthias J. Sax, Don't Repeat Yourself: Introducing Exactly-Once Semantics in Apache Kafka Kafka Summit London, 2018-04. Checked 2026-09-01.
  31. Malcolm Featonby, Making retries safe with idempotent APIs Amazon Builders' Library, 2021-01. Checked 2026-09-01.
  32. AWS, Ensuring idempotency in Amazon EC2 API requests docs.aws.amazon.com, current; ClientToken introduced 2010. Checked 2026-09-01.
  33. AWS News Blog, New Amazon EC2 feature: Idempotent instance creation aws.amazon.com, 2010. Checked 2026-09-01.
  34. boto/boto3 issue #4406: Unexpected breaking change in RunInstances github.com/boto/boto3, 2025-01-26. Checked 2026-09-01.
  35. AWS, DynamoDB TransactWriteItems ClientRequestToken docs.aws.amazon.com, current. Checked 2026-09-01.
  36. Adyen, API idempotency docs.adyen.com, current. Checked 2026-09-01.
  37. Square, Idempotency developer.squareup.com, current. Checked 2026-09-01.
  38. Sidekiq wiki, Best Practices github.com/sidekiq/sidekiq, current. Checked 2026-09-01.
  39. Hatchet, The First Idempotency Key hatchet.run/blog, 2026. Checked 2026-09-01.
  40. danluu et al., A collection of postmortems github.com/danluu/post-mortems, maintained. Fetched in full. Checked 2026-09-01.