SyncTrix logoSyncTrix
All articles
Platform10 min read

Idempotency keys: designing operations that survive being retried

Every network call can be retried, including the one that already succeeded. Here's how to make operations safe to repeat without double-charging anyone.

By Marcus Hale
Idempotency keys: designing operations that survive being retried

A request times out. You do not know whether it failed before the work happened, after it happened, or while the response was in flight. Retrying might be harmless or might charge a customer twice, and the only way to make that decision safe is to design the operation so repeating it cannot repeat its effects.

01The timeout tells you nothing

A timeout is the absence of information. The work may have completed perfectly with only the response lost. Any system that treats a timeout as failure and retries blindly will eventually duplicate an effect that matters.

This is why exactly-once delivery does not exist in the way people want it to. What is achievable is at-least-once delivery combined with idempotent processing, which produces exactly-once effects - the guarantee everyone actually needs.

02How an idempotency key works

The client generates a unique key per logical operation and sends it with the request. The server records the key alongside the result, and on seeing it again returns the stored result rather than performing the work a second time.

The critical detail is that the key must be recorded in the same transaction as the effect. If you charge the card and then record the key in a separate step, a crash between the two leaves you with the charge and no record - and the retry charges again. That ordering is the entire mechanism.

03Choosing the key

The key must be stable across retries of the same logical operation and different across distinct ones. Generated by the client before the first attempt, reused for every retry of that attempt, never reused for a new intent.

Deriving the key from request content is tempting and usually wrong. Two genuinely separate payments of the same amount to the same recipient on the same day are legitimately different operations, and a content hash would collapse them into one - silently losing a payment rather than duplicating one, which is arguably worse.

  • Generate client-side, before the first attempt.
  • Reuse for every retry of that operation, never for a new one.
  • Avoid deriving from content - legitimate duplicates exist.
  • Scope keys per tenant so one client cannot collide with another.

04What to store and for how long

Store the key, the resulting response, a status, and a timestamp. Returning the stored response matters: a client that retries and gets a bare acknowledgement instead of the original result cannot reconcile its own state.

Retention should exceed your longest realistic retry window - a client retrying after a multi-day outage should still be recognised. Beyond that, expire records so the table does not grow without bound, and be explicit about that boundary rather than leaving it implicit.

FieldPurpose
KeyClient-supplied identifier, unique per operation
StatusIn-progress, completed or failed
ResponseStored result returned on replay
Request fingerprintDetects the same key reused with different parameters
Created atDrives expiry
Idempotency record fields

05The concurrent-retry case

Two retries can arrive simultaneously - a client gives up and retries while the original is still processing. Both see no completed record and both proceed, which is exactly the duplication the mechanism exists to prevent.

Solve it with a uniqueness constraint on the key, inserted at the start with an in-progress status. The second request fails the insert and can wait or return a conflict, and correctness comes from the database rather than from application timing.

06The same key with different parameters

A client reusing a key with a different payload is a bug on their side, and the safe response is to reject it rather than guess which request was intended. Store a fingerprint of the original request and compare on replay.

Returning the original result would silently ignore what the client asked for; performing the new operation would break the guarantee. An explicit error is the only honest option, and it surfaces the client bug rather than hiding it.

  • Fingerprint the original request alongside the key.
  • Reject mismatches explicitly rather than guessing.
  • Never silently return the old result for different parameters.
  • Make the error message name the mismatch, so it is debuggable.

Topics

idempotency keyidempotent api designexactly once processingretry safetyduplicate payment preventiondistributed systems retries

Marcus Hale

Lead Architect · SyncTrix

Writes about the engineering decisions behind production systems - architecture, delivery and the trade-offs that only show up at scale.

Building something like this?

SyncTrix engineers AI, SaaS, platform and cloud systems for enterprises and high-growth teams. Tell us what you're shipping and we'll scope it with you.