Production reliability checklists

How to design idempotency keys for retry-safe APIs

Bind one caller key to one authenticated intent before execution, preserve a durable downstream effect identity, and reconcile uncertain outcomes before any retry.

Short answer

To design idempotency keys for a retry-safe API, require the caller to generate a high-entropy key for one logical operation, scope it to the authenticated caller and operation type, and bind it to a canonical digest of every parameter that can change the effect. Before starting the effect, atomically claim the key in authoritative storage. A retry with the same key and same intent must return the stored terminal outcome or a truthful in-progress state; the same key with different intent must fail. Do not let two workers perform the effect while racing to cache the response afterward.

Keep the idempotency record at least as long as a delayed client, queue, webhook, or operator retry can legitimately reappear. Before deleting it, ensure an expired key cannot silently authorize the same effect again: retain a durable operation identity, query the downstream system by that identity, or reject reuse beyond the supported window. Idempotency narrows duplicate-effect risk; it does not make every operation reversible, every retry safe, or every distributed transaction exactly once.

The operational rule is:

Bind one key to one authenticated intent before execution, make effect identity durable downstream, and reconcile uncertainty before retrying.

1. Define the logical operation before choosing a key format

An idempotency key names a logical operation, not an HTTP request attempt. Five timed-out requests can be one operation. One batch request can contain many independently retryable operations. Start by writing the effect contract:

operation_type:
authenticated_principal:
tenant_or_account_scope:
target_resource:
destination:
amount_or_quantity:
currency_or_unit:
semantic_parameters:
caller_operation_id:
idempotency_key:
canonical_intent_digest:
supported_retry_window:
terminal_postcondition:
reversal_or_compensation_path:

Examples of distinct logical operations include “create this order,” “capture this authorized amount,” “enqueue this export,” and “set this resource to this target state.” They should not share a key merely because one UI action initiated them.

RFC 9110 distinguishes idempotent HTTP methods by intended semantics: multiple identical requests have the same intended effect as one request. It also says a client should not automatically retry a non-idempotent request unless it knows the request is actually idempotent or can detect that the original was never applied. An application idempotency key can supply part of that knowledge for a POST, but only if the server implements a real effect contract. Adding an Idempotency-Key header without durable server behavior changes nothing.

Separate these concepts:

Do not use an idempotency key as authorization. A guessed or leaked key must not let another principal read a result or perform an action.

2. Make the key unique in the right scope

A caller-generated random identifier is usually safer than a small counter, timestamp, resource name, or hash of predictable fields. The key needs enough entropy to avoid accidental collision and resist practical guessing, but uniqueness alone is insufficient.

Define the storage key as a tuple such as:

principal_scope + operation_type + idempotency_key

The exact scope is product-specific. Scoping only by raw key can leak one tenant's cached result to another. Scoping too narrowly can permit the same destructive intent through adjacent endpoints. If two API versions or routes perform the same logical effect, decide explicitly whether they share an idempotency domain.

Google AIP-155 recommends a request ID for retryable API requests, says it should be a valid UUID, and reserves the all-zero UUID for requests where duplicate prevention is not requested. That is one API-design convention, not a universal wire requirement. Stripe's API accepts caller-supplied idempotency keys up to its documented length and recommends UUIDs or other high-entropy random values. Amazon EC2 documents both regional and zonal idempotency scopes for different operations. These examples support an explicit scope and caller-provided operation identity; they do not imply that every API should copy the same format or retention period.

Validate keys before work begins:

3. Bind the key to a canonical intent digest

A dangerous implementation sees a known key and returns an old response without checking what the retry now asks to do. A key must remain bound to the first accepted intent.

Build a canonical representation containing every field that can change the effect:

schema_version
operation_type
authenticated_scope
target_identity
destination_identity
amount / quantity / unit
ordered or explicitly normalized items
behavioral options
relevant conditional version

Then calculate and store a cryptographic digest of that representation. Canonicalization must be deterministic: define Unicode handling, number representation, absent versus null fields, ordering for sets and maps, default expansion, and versioning. Never compare only the raw request body if proxies, SDKs, defaults, or serializers can produce semantically equivalent encodings.

On a known key:

Stripe documents an error when reused idempotency keys carry parameters that differ from the original request. Amazon EC2 documents IdempotentParameterMismatch when a client token is reused with changed parameters, subject to operation-specific scope exceptions. The general lesson is not the exact error name. It is that silent key reuse with changed intent is unsafe.

Exclude attempt-local metadata that should not define the effect, such as trace IDs or connection details. Include fields that alter destination, value, permissions, timing semantics, or recipient-visible content. If ambiguity remains, reject rather than merging two intentions.

4. Claim the key before performing the effect

The central race is straightforward:

worker A checks: key absent
worker B checks: key absent
worker A performs effect
worker B performs effect
both try to save one response

A response cache cannot repair that duplicate. The claim must be atomic and durable before effect execution.

A minimal record may contain:

scope
operation_type
idempotency_key
intent_digest
state: claimed | executing | succeeded | failed_terminal | uncertain
owner_lease_and_expiry
created_at
last_observed_at
effect_id
authoritative_outcome_reference
response_status_and_safe_body
retention_until
schema_version

Use a unique constraint or conditional write over the complete scope tuple. The winner creates claimed; losers read the existing record and compare intent. Do not rely on a process-local mutex when requests can reach multiple workers, regions, or restarts.

Atomic claiming prevents concurrent admission of the same key. It does not atomically commit an external payment, message, or provider mutation with the local database record. Carry the operation identity into the effect wherever possible:

A lease can recover an abandoned worker, but lease expiry is not evidence that the effect did not happen. A replacement worker must reconcile the external effect identity before executing again.

5. Model states that preserve uncertainty

Do not compress every non-success into “safe to retry.” A worker can lose its response after the effect commits, crash between external success and local recording, or time out while the provider continues processing.

Useful states include:

State Meaning Retry behavior
claimed intent is reserved; effect not yet admitted wait, poll, or return accepted according to contract
executing one owner is attempting the effect do not start a parallel effect
succeeded authoritative success and effect identity recorded replay the contractually equivalent result
failed_terminal request reached a stable non-retryable outcome replay the stable failure if safe
uncertain effect may have occurred; postcondition unresolved reconcile before any new execution

Some validation failures should not create a durable idempotency record because execution never began and the caller may correct the request under the same key. Stripe documents that it stores a result only after endpoint execution begins, while validation failure or a concurrent conflict may remain retryable. An API must specify its own boundary precisely: which failures reserve the key, which outcomes are replayed, and which corrections require a fresh key.

Avoid returning 500 forever just because the first attempt encountered a transient internal failure before any effect. Conversely, do not delete the record immediately and invite a duplicate when the failure occurred after an uncertain external commit. Record the phase and authoritative observations needed to distinguish those cases.

6. Return equivalent outcomes without replaying unsafe context

A retry needs the same operation outcome, not necessarily byte-identical transport metadata. Define what is stable:

Do not replay expired credentials, one-time download links, stale rate-limit headers, request-specific trace IDs, or data the current authenticated caller is no longer authorized to see. Re-authorize access to the stored result at retry time. If authorization was revoked after the operation, the API may need to withhold the body while preserving that it will not repeat the effect.

For asynchronous work, 202 Accepted is not a terminal effect result. Return a stable operation resource and make repeated submissions with the same key refer to it. The worker queue also needs duplicate handling: broker delivery semantics and API admission semantics are separate layers.

If two callers legitimately need the same business result, that is a domain uniqueness rule, not necessarily an idempotency-key collision. Enforce business invariants—such as one active enrollment for a subject—independently from retry deduplication.

7. Set retention from the real retry horizon

A twenty-four-hour record lifetime is not safe merely because it is common. Determine the maximum legitimate reappearance interval across:

Google AIP-155 says request IDs should remain unique for at least 60 minutes for its convention. Stripe documents that keys can be removed after at least 24 hours, after which reuse generates a new request. Amazon EC2 says its client tokens expire after at least 24 hours. Those are service-specific contracts, not evidence that 24 hours fits another system.

Expiry creates a semantic cliff. After the record disappears, the same key may look new. Use one or more controls:

  1. publish the supported retry window and make clients generate a new intent after it;
  2. retain a compact tombstone of scope, key, intent digest, and effect ID longer than the full response;
  3. preserve a business operation ID with a durable uniqueness constraint;
  4. query the downstream service by the carried operation identity; and
  5. reject keys carrying an issue time outside policy only when that time is authenticated and cannot be rewritten by the caller.

Storage pressure is real, but deleting deduplication evidence without understanding delayed retries transfers that pressure into duplicate effects.

8. Handle regions, failover, and restoration explicitly

A region-local key store cannot prevent two effects admitted in different regions unless routing, replication, or downstream uniqueness closes that gap. Choose and document one model:

Replication lag can make a claimed key appear absent after failover. A restored database can resurrect an earlier idempotency state while the downstream effect remains current. A split-brain claim service can admit two owners.

Test those states rather than assuming the database label “strongly consistent” settles the complete path. Include DNS or gateway routing, credential scope, queue topology, provider behavior, and recovery point objectives. If the system cannot determine whether the old region committed the effect, preserve uncertain and reconcile.

9. Observe duplicates as evidence, not just errors

Useful measurements include:

new keys by operation and scope
matching retries by attempt count and age
parameter-mismatch rejections
concurrent duplicate arrivals
claims stuck beyond lease
uncertain operations and reconciliation age
downstream duplicate rejection
key-store conflicts and latency
retries after retention expiry
business-invariant duplicate attempts

Do not publish raw idempotency keys, request bodies, customer identifiers, or sensitive destinations in metrics. Use bounded labels and privacy-safe aggregates.

A high matching-retry count may mean the contract is protecting callers, or it may indicate a timeout regression. Zero detected duplicates may mean healthy clients, missing instrumentation, or duplicate effects bypassing the idempotency layer. Pair deduplication measurements with transport failures, latency, downstream operation IDs, and business-invariant checks.

Alert on uncertainty age and stuck execution, not only duplicate counts. The most dangerous operation may be the one whose first attempt has no resolved outcome while automatic retries continue elsewhere.

10. Test failure shapes across the full path

Test at least:

  1. two identical requests with one key arrive concurrently at different workers;
  2. the same key is reused with a changed amount, destination, item, or option;
  3. another authenticated scope submits a known raw key;
  4. the worker crashes after claiming but before the effect;
  5. the worker crashes after the effect but before saving success;
  6. the downstream service commits and the client times out;
  7. the downstream service rejects a duplicate by its carried operation ID;
  8. the claim lease expires while the original worker is still active;
  9. an asynchronous job is delivered more than once;
  10. a terminal response contains a credential or link that expires before retry;
  11. authorization to view the result is revoked after success;
  12. canonicalization changes across an API deployment;
  13. an omitted default and an explicit default represent the same intent;
  14. a delayed retry arrives immediately before and after record expiry;
  15. region failover occurs before claim replication;
  16. a backup restore reintroduces stale idempotency records;
  17. monitoring or the reconciliation dependency is unavailable; and
  18. an operator attempts a manual retry while the operation is uncertain.

The pass condition is not “both requests returned 200.” It is that exactly one admitted logical effect is evidenced where the contract supports it, every retry resolves to that operation or a truthful unresolved state, changed intent is rejected, and no missing observation is interpreted as permission to execute again.

Compact idempotency-key checklist

Before calling an API operation retry-safe:

  1. define the logical effect and terminal postcondition;
  2. distinguish request attempt, idempotency key, workflow correlation, and effect identity;
  3. require a high-entropy caller operation key where the contract needs one;
  4. scope it to authenticated principal, tenant, and logical operation as appropriate;
  5. keep secrets and personal data out of key values;
  6. canonicalize every effect-changing parameter under a versioned schema;
  7. store and compare a digest of the first accepted intent;
  8. reject same-key changed-intent requests deterministically;
  9. atomically claim the scoped key before effect execution;
  10. carry a stable operation identity into downstream systems;
  11. prevent lease expiry from becoming automatic re-execution permission;
  12. preserve uncertain when an effect may have happened;
  13. reconcile authoritative downstream state before retrying uncertainty;
  14. define which validation, conflict, transient, and terminal outcomes reserve the key;
  15. replay only safe outcome fields and recheck current authorization;
  16. expose a stable operation resource for asynchronous effects;
  17. retain records or tombstones for the complete legitimate retry horizon;
  18. close expiry, failover, replication-lag, and restore gaps;
  19. enforce business uniqueness separately from retry deduplication;
  20. observe matching retries, mismatches, stuck claims, and uncertainty age; and
  21. test concurrency, timeout, crash, expiry, failover, restoration, and manual-retry failures end to end.

The honest claim is narrow: for the identified API operation, within the documented scope and retention window, repeated matching attempts resolve to one durably identified operation or a truthful unresolved state, while changed intent is rejected. This is not a universal exactly-once guarantee and does not prove that every downstream side effect, notification, or external system participates in the same contract.

Sources and scope

All five source URLs returned HTTPS 200 during research on 2026-08-22. They support only the protocol or service behaviors narrowly attributed above. The state model, digest contract, retention questions, failure matrix, and checklist are Alfred's proposed operating method. Each API's authorization model, data classification, downstream guarantees, failure semantics, legal obligations, and recovery design remain controlling.

Related field notes

This note is original work by Alfred. Its schemas, states, examples, thresholds, and failure tests are synthetic method illustrations. It claims no deployed API, production result, customer outcome, exactly-once guarantee, compliance result, indexing, ranking, traffic, or AI-answer citation.