On this page
A timeout does not tell a client whether an order was created. The server might have committed the transaction and lost the response, or the request might never have reached the application. Blindly retrying can create duplicate work. Refusing all retries leaves users unsure whether they paid or submitted anything at all.
Define the operation's retry contract
An idempotency key lets a caller label one logical operation across multiple delivery attempts. The server records the operation's outcome and recognizes later requests with the same identity. This is especially useful for POST endpoints creating orders, starting transfers, or provisioning resources.
Sketch the request and response with the JSON Formatter. Identify which fields determine the operation, which values are assigned by the server, and what successful replay must return. Keep examples synthetic: real checkout fixtures often contain private customer information.
Specify where callers provide the key, its allowed format and length, and how long your server remembers it. A header named Idempotency-Key is common, but its existence does not automatically define behavior. The retention period, concurrent-request policy, and handling of changed request bodies remain part of your API's explicit contract.
Scope and fingerprint requests correctly
Scope stored keys to the authenticated tenant and logical operation. Two accounts using the same string should not see each other's responses. Reusing a key on a different endpoint should either be rejected or treated as a different namespace according to documentation. Apply authorization before revealing a stored outcome.
Store a request fingerprint so the same key cannot silently mean two different orders. Hash a deterministic representation of the operation's relevant input, including meaningful path parameters and version context. Hashing raw JSON bytes treats harmless formatting differences as different requests; canonicalizing carelessly may erase meaningful distinctions.
CREATE TABLE idempotency_records (
tenant_id bigint NOT NULL,
operation text NOT NULL,
request_key text NOT NULL,
request_hash text NOT NULL,
state text NOT NULL,
response_status integer,
response_body jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (tenant_id, operation, request_key)
);This schema is a starting point, not a complete implementation. Add checks, retention indexes, and response metadata for your contract. Never store authorization tokens or cookies in the replay record. Decide how to handle decimal values and optional fields before choosing a canonical representation.
Coordinate duplicate arrivals atomically
Two requests with the same key can arrive simultaneously. A read-then-insert approach races: both may find no record and both may perform the side effect. Use a database uniqueness constraint to establish one winner, then make the loser follow the documented replay or in-progress behavior.
For effects entirely inside one database, create the idempotency record, perform the operation, and save its result within a transaction where practical. Another request should not observe a completed marker before the corresponding order is durable. Read the database's actual isolation rules rather than assuming transactions eliminate every race; PostgreSQL transaction isolation documents important differences.
An in-progress request can make later duplicates wait within a strict timeout, or return a documented conflict with retry guidance. Do not leave callers blocked indefinitely while a slow upstream system runs. If you use leases, include ownership or fencing safeguards so an expired worker cannot later commit conflicting work after another worker takes over.
Decide what to record for failures. A syntax or validation failure before execution may be safe to retry after correction with a new key. A dependency timeout after a possible external side effect is fundamentally different: you must reconcile that uncertain outcome, not casually permit another attempt to create the effect.
Handle external side effects and response replay
A local transaction cannot roll back a payment already accepted by another service. Pass a stable downstream idempotency key when the provider supports one, or use a durable workflow that can query and reconcile the external operation. Consider an outbox for reliable dispatch, but remember it provides durable delivery rather than magical exactly-once execution everywhere.
Store the status and response fields needed for a semantically equivalent replay. Replaying an original 201 with the same resource identifier is often clearer than inventing an unrelated response. Decide whether time-sensitive links must be regenerated, and document that behavior. Never replay stale Set-Cookie headers or request-specific authentication data indiscriminately.
The Stripe idempotent requests guide demonstrates one concrete policy; it is not a universal rule for every API. Its value is the precision with which reuse and outcomes are described.
Use payload mismatch errors when the same key arrives with different operation data. Returning the original success without warning can convince callers that a new purchase used different quantities or shipping details. A conflict should clearly explain that the caller must use a new key for a genuinely new operation.
Test retention and ambiguous failures
Run concurrent requests with one key and verify that only one business object is created. Disconnect clients after submission and before reading the response, then retry. Crash the worker before and after each database commit and external call. These tests reveal the real guarantees more effectively than a happy-path demonstration.
Set retention limits based on the realistic retry window and business risk. Once a record expires, an old key may create a new operation. State that clearly, and consider a stronger permanent business constraint for effects such as one charge per invoice. A short-lived cache alone is rarely sufficient protection for durable financial invariants.
Inspect replay bodies with the JSON to TypeScript tool, then maintain explicit types for completed, pending, and conflict outcomes. Generated types describe example structure, not runtime validation or transaction safety. Cover the retry contract in your Backend & APIs documentation.
Finally, distinguish idempotent method semantics from implementation technique using RFC 9110. A retry-safe business operation still needs authorization, rate limits, and resource constraints. Keys prevent accidental duplicate intent; they are neither credentials nor a general abuse-prevention mechanism.
Key takeaways
- Bind each key to a tenant, operation, and deterministic request fingerprint.
- Use atomic uniqueness and durable state rather than process-local deduplication.
- Reconcile uncertain external effects instead of assuming failed responses mean failed work.
- Document concurrent requests, replay behavior, mismatches, and key expiration precisely.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor