> ## Content Index
> Fetch the complete content index at: https://debugly.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Reviewing the Retry That Had No Idempotency Key
- URL: https://debugly.dev/reviewing-retry-without-idempotency-key/
- Published: 2026-10-09T16:01:00.000Z
- Updated: 2026-10-17T05:55:56.000Z
- Description: The client retried the failed payment request on a timeout, and the original had succeeded, so the retry charged the customer a second time for one order…
- Author: Rohit Bhadani
- Tags: Code Review, Payments, Reliability

The twice behaved. The incident behaved. The chain between them is where working as designed stopped meaning working.

I have hit this enough times that I now check for it before I check for anything clever.

Strip the retry without the idempotency key down and one property makes it unavoidable rather than merely unlucky: a timeout does not tell the client whether the request was processed, so the client must guess, and the guess is wrong half the time by construction.

This was a payment integration, and every non-idempotent operation retried on an ambiguous failure behaves the same way.

## What the review saw

The review saw the retry, and the retry was the resilience, and the resilience was the good, and the good was the pass, because the retry is the correct response to the transient failure, and the transient is the network, and the network is the unreliable, and the unreliable is the retry's justification, and the justification was the review's criterion, and the criterion was met.

The review did not ask what the retry does when the original succeeded, and the ask is the question, and the question is the idempotency, and the idempotency is the key, and the key is the deduplication, and the deduplication is the second request's no-op, and the no-op is the safety, and the safety was the absent, and the absent was the incident.

The absent is the review's blind spot, because the retry is present and the retry looks right, and the right is the shape, and the shape is the pass, and the pass is the double charge, and the charge is the money, and the money is the reason the review should ask what happens when the ambiguous failure was actually a success.

## Why the timeout is ambiguous

The timeout is the response's absence, and the absence is the two possibilities, and the two are the request's loss and the response's loss, and the loss is the not processed and the processed, and the two are the opposite, and the opposite is the ambiguity, and the ambiguity is the client's ignorance, and the ignorance is the guess, and the guess is the retry, and the retry is the second processing, and the processing is the charge.

The client cannot distinguish, because the distinguishing would require the response, and the response is the absent, and the absent is the timeout's definition, and the definition is the ambiguity, and the ambiguity is the unresolvable, and the unresolvable is the key's necessity, and the necessity is the fix, and the fix is the identifier that lets the server recognise the repeat.

The key is the client's generated identifier, and the identifier is the request's identity, and the identity is the server's memory, and the memory is the first request's result, and the result is the second request's response, and the response is the no second processing, and the no processing is the safety, and the safety is the fix.

This is the same shape as [the coupon that applied twice when the customer retried](https://debugly.dev/coupon-applied-twice-on-retry/), which is why the two fixes look alike.

## The questions that catch it

**What happens if the original succeeded?** The answer is the duplicate, and the duplicate is the charge, and the charge is the money, and the money is the fix's target, and the target is the key, and the key is the fix, and the question is the review's first, because the retry's safety depends on the answer.

**Is the operation idempotent by nature?** The answer is the GET's yes and the POST's no, and the no is the key's requirement, and the requirement is the fix, and the fix is the explicit key, and the explicit is the discipline, and the question is the review's second.

**What does the client do with a 5xx versus a timeout?** The answer should differ, and the differ is the 5xx's known failure and the timeout's unknown, and the unknown is the key's case, and the case is the fix, and the question is the review's third, because the two failures need different handling.

**How many retries, and with what backoff?** The answer is the bound, and the bound is the storm's absence, and the absence is the fix, and the fix is the exponential with jitter, and the jitter is the decorrelation, and the question is the review's fourth.

## The fix

**Added the idempotency key to every payment request.** The key is generated once per logical operation and reused on the retry, and the reused is the server's recognition, and the recognition is the no duplicate, and the no duplicate is the fix, and the fix is the header, and the header is the one line.

**Made the retry reuse the key rather than regenerate it.** The regeneration is the new identity, and the new identity is the second processing, and the processing is the duplicate, and the duplicate is the bug, and the bug is the subtle, because the key exists but the retry does not reuse it, and the not reuse is the fix's detail.

**Stored the key with the operation.** The key is the order's field, and the field is the retry's source, and the source is the consistency, and the consistency is the fix, and the fix is the persistence, and the persistence is the discipline, because the key generated per attempt is the key that does not deduplicate.

**Distinguished the timeout from the explicit failure.** The explicit failure is the safe retry, and the timeout is the key's requirement, and the requirement is the handling, and the handling is the fix, and the fix is the branch, and the branch is the review's catch.

## The rule

A timeout does not tell the client whether the request was processed, so retrying a non-idempotent operation without an idempotency key risks a duplicate effect. Generate one key per logical operation, reuse it on every retry, persist it with the operation, and handle timeouts differently from explicit failures.

The customer was charged twice for one order because the client retried a payment request that had already succeeded, and the retry was correct resilience with no way for the server to recognise the repeat. That is the whole pattern, and it is the part worth remembering.

---

Published on [debugly.dev](https://debugly.dev/) by [Rohit Bhadani](https://debugly.dev/about/) | Founder of [Krova Cloud](https://krova.cloud/?ref=debugly.dev)