All posts
idempotencyapi designretriesoperations

How long should an idempotency key live

The retention window is part of the contract, because replay safety ends the moment the original decision record disappears

Teams talk about idempotency keys as if the hard part is only generating them and checking them on write.

That is the opening move, not the whole contract.

The more operational question arrives later: how long does the system remember the key, the first decision attached to it, and the response a legitimate retry should receive?

If the answer is unclear, the API may look idempotent during a short retry storm and become ambiguous the next morning.

Retention is part of the public behavior

An idempotency key is only useful while the system still remembers what it already did with it.

Suppose a client times out after sending a charge request, backs off, and retries a few minutes later. Good. The server still has the key, still knows the original outcome, and can replay the same result or direct the caller to the original receipt.

Now suppose the same client retries after the retention window has quietly expired.

At that point one of three bad things often happens:

  • the server treats the request as brand new and risks duplicating the side effect
  • the server rejects the request without enough evidence for the caller to recover cleanly
  • the server accepts the key but no longer has enough original state to answer honestly

None of those are minor implementation details. They are contract behavior.

That is why the retention window belongs in the design, not as an afterthought buried in a cache TTL someone picked because it looked cheap.

The window should match the recovery story

There is no universal correct duration.

The right question is narrower: how long can a legitimate client still be trying to learn whether the original request landed, and what recovery path do you want to support during that period?

Payment-like operations usually need longer memory than a lightweight preference update. A background job enqueue may need enough time to survive queue lag plus client retry backoff. A webhook receiver may need to remember keys for as long as the upstream sender is documented to retry.

If the business action can create money movement, inventory change, account mutation, or customer-visible duplication, the cheap answer is usually the wrong answer.

A short TTL can look efficient while pushing replay risk back onto support, operators, and customers.

The retention window should therefore be chosen together with:

  • the maximum legitimate retry horizon
  • the side-effect cost of duplicate execution
  • whether the original response body must be replayed exactly
  • what receipt or lookup path exists after the key expires

Those decisions belong together because they describe one recovery policy.

Expiry should not erase the evidence too early

Teams sometimes store the key but not enough of the original decision.

They keep a hash, a timestamp, and maybe a success flag. Then a later retry arrives and the system knows the request existed but cannot prove what happened strongly enough to answer well.

That is a weaker form of the same bug.

The idempotency record should normally preserve enough evidence to support the promised retry behavior. Depending on the API, that may include the normalized request fingerprint, the original outcome class, the response body or receipt identifier, and the side-effect identity needed for later lookup.

If storage cost becomes the argument against that evidence, be honest about the trade. Do not keep pretending the endpoint is fully replay-safe while the proof disappears underneath it.

A thinner record can be acceptable, but only if the fallback recovery path is explicit and trustworthy.

After expiry, say what happens next

Expiry is not failure by itself.

The problem is silent expiry with no stated post-expiry behavior.

A caller needs to know whether an expired key means:

  • the operation can be retried safely with a fresh key
  • the original operation must be looked up by receipt or resource ID first
  • the request is too dangerous to replay automatically

If the API leaves that unclear, clients guess. Guesses around side effects are how duplicate work becomes incident response.

Good idempotency design therefore has two phases:

  1. While the key is retained, the system can answer from the original decision record.
  2. After the key expires, the system points the caller to the next safe proof boundary.

That second phase is where many designs are still vague.

The retention window should survive handoff

The engineer who built the endpoint may remember that keys live for twenty four hours and that retries after that should resolve through the receipt lookup route.

The next engineer, support lead, or integration partner will only know what the system documents and returns.

If expiry behavior is not obvious, later operators start reconstructing intent from caches, logs, and half-remembered architecture notes. That is too much archaeology for something as ordinary as a retry.

Reliable dispatch systems keep the recovery story beside the interface:

  • how long the key is honored
  • what evidence is retained during that time
  • which response is replayed
  • what the caller should do after expiry

That is not overdesign. It is the minimum needed to stop idempotency from becoming a short-lived illusion.

At Stack Dispatch we care about this because retries are only safe when the decision record outlives the doubt that created the retry. An idempotency key without a clear lifetime is not a full contract yet. It is a promising start with an unpriced failure mode waiting at the edge of the TTL.

0 comments

Join the conversation

Enjoyed this? Subscribe for more.

Get new essays on software architecture, AI systems, and engineering craft delivered to your inbox. No spam-ever.