Skip to content
Back to blog
Production note•Sep 24, 2026

A retry is a new request, not a new intention

How idempotency keys, database constraints, and honest UI states prevent double actions in real products.

Software EngineeringReliabilityAPI DesignProduct

The user clicked once; the server saw twice

A request can time out after the server commits a write but before the browser receives the response. The user sees a spinner, clicks again, or the client retries automatically. Now the server receives a second request for the same intent.

This is how duplicate payments, applications, contact requests, and notification emails appear. The network has no way to tell the server that the user's intention was already fulfilled. The application has to model that.

Give the intention an identity

For a sensitive write, the client can generate a stable idempotency key when the user begins the action. Reuse that key for retries of the same action. Generate a new key only for a genuinely new action.

The server stores the key with the outcome and enforces a unique constraint on its scope, often (actorId, key). If the same request arrives again, return the recorded outcome instead of repeating the side effect.

Request: actor A, key K, create application
 
No record for (A, K) -> perform write, record result
Record for (A, K)    -> return previous result
Same key, new payload -> reject as a conflicting request

The last line matters. A key should not silently refer to two different payloads. Store a hash of the important request fields and compare it on retry.

The database constraint is the final guard

Disabling the submit button is good feedback, but it is not a concurrency guarantee. Two tabs can submit at the same time. A mobile client can retry after reconnection. A browser can resend a request.

Put the uniqueness rule in the database and handle a conflict by looking up the existing result. An application-level "check, then insert" without a constraint still races when two requests pass the check together.

For multi-step operations, use a transaction for database changes that must succeed together. External effects such as email or payment calls need their own strategy, often an outbox or a provider-level idempotency key, because a database transaction cannot roll back an email already sent.

Retries also need a deadline

Not every error is retryable. Retry a transient network failure with a bound on attempts and delay between attempts. Do not retry validation failures or permission denials. If the server says an action is still processing, show that state instead of creating a second operation.

The UI should tell the truth: "Still checking your submission" is different from "Submission failed." If the outcome is uncertain, offer a way to look it up by reference ID.

Where this pays off

Use this pattern where duplication causes real harm: money, inventory, invitations, applications, and outbound messages. A harmless view preference probably does not need a key. The implementation cost should match the consequence of repeating the action.

Reliable software does not assume requests arrive exactly once. It makes the user's intention explicit enough to survive duplicates.

If you are building a web product and want it faster, cleaner, and easier to operate, I am open to collaborations.