A payment request times out. Your client does not know whether the charge went through, so it retries. The provider processed the first one fine — the response just never came back. The customer is now charged twice, and you find out from support, not from monitoring.
Retries are not the bug
The instinct is to make retries smarter: longer timeouts, fewer attempts, a circuit breaker. None of that helps. Networks lose responses, and any system that gives up after a timeout without knowing the outcome has to either retry or leave money in an unknown state.
The property you want is that sending the same request twice has the same effect as sending it once. That is a design decision made before the first integration, not a patch applied after the first incident.
What the key actually is
An idempotency key identifies an intent, not a request. It is minted once, when the user presses the button, and reused across every retry of that same intent. Generate it client-side and store it with the attempt.
Server side, the key goes in a unique index alongside the response you returned for it:
idempotency_records
key unique
request_hash
response_body
status
created_at
On arrival: look up the key. Miss — process, store, return. Hit — return the stored response without touching the ledger. Hit with a different request_hash — reject loudly, because the client reused a key for different content and that is a bug you want to hear about.
Where teams get it wrong
Generating the key server-side. That defeats the point entirely: a retry generates a new key and processes again.
Keying on the payload hash instead of an explicit key. Two legitimate identical charges — same customer, same amount, same minute — collapse into one, and you have silently lost a real payment.
Expiring records too early. Twenty-four hours is a reasonable floor. A client retrying after a long outage is exactly the case this exists for.
The cheap version
One table, one unique index, one lookup at the top of the handler. It costs an afternoon before launch, and an incident with real money in it afterwards.