0% прочитано

How to Tell a Retried Request from a New Operation

If a response is lost, the client should be able to safely retry the same operation. The server must distinguish that retry from a new intent and avoid duplicate side effects.

5 жовтня 2026 р. 8 хв читанняLaravel

A client sends a request. The server completes the operation. Then the connection fails before the response reaches the client.

The client does not know what happened.

From its perspective, two outcomes are equally possible: the server never received the request, or the server completed it and only the response was lost.

The natural reaction is to retry.

This is where idempotency becomes important: how can the server tell that this is a retry of an already completed operation rather than a new operation with the same data?

Identical request data does not prove that two requests represent the same operation. Two separate user actions may legitimately contain identical values.

A Lost Response Does Not Mean the Operation Failed

A typical retry scenario
Client                  Server    |                       |   |------ request ------->|   |                       |   |                  operation   |                  completed   |                       |   |<----- response -------X   |   |    response lost   |   |------ retry --------->|

If the server simply executes the second request again, the result depends on the kind of operation.

For reads, that is often safe.

For state-changing operations, a retry may create a second record, a second charge, a second message, or another duplicated side effect.

A client retry therefore should not automatically mean re-execution.

I Do Not Identify Retries by Payload Equality Alone

Identical parameters do not identify intent
Request A amount = 100currency = UAH  Request B amount = 100currency = UAH  Identical values may mean: 1. retry of request A2. a separate new operation

Comparing every request field may look like an easy way to detect retries.

But that mixes two different ideas: equal data and equal intent.

A user may legitimately perform two identical operations one after another.

The server therefore needs a separate way to associate a network retry with the same logical operation.

A Retry Should Preserve the Identity of the Logical Operation

A network attempt and a logical operation are not the same thing
New user intent        |        vnew logical operation        |        +--> first attempt        |        +--> retry after timeout        |        +--> another retry  All attempts belongto one operation.  

This is the core model I use.

A network request is only an attempt to deliver an operation.

One logical operation may require one, two, or more such attempts.

While the client retries the same operation, it preserves the same stable operation identity.

When the user actually starts a new operation, a new identity is created.

The Server Checks Whether It Has Seen the Operation Before

PHP - Simplified result reuse
$result = $operations->find(    $request->operationIdentifier(),); if ($result !== null) {    return $result->response();} $result = $service->execute(    $request->input(),); $operations->remember(    $request->operationIdentifier(),    $result,);

This example is intentionally simplified.

The important part is not the exact class or identifier format.

The important part is that a retry of a known operation does not execute the business action again.

The server instead returns the result associated with the first successful execution.

The Same Operation Identity with Different Input Is an Error

The same operation identity cannot represent a different intent
First attempt operation = Xamount = 100  Retry operation = Xamount = 100 -> same intent  Different request 

A stable retry identity should not become a way to accidentally execute a different action.

I therefore also verify that a retry represents the original intent.

If the same identity arrives with different meaningful input, it is not a retry.

I treat that request as conflicting and do not execute a new operation under the old identity.

A Retry May Arrive While the First Attempt Is Still Running

A retry does not always arrive after completion
Request A    |    +--> operation started    |    |        Request B    |           |    |           +--> same operation    |           +--> first attempt still running    |    +--> completion

Another important case is that two attempts may overlap in time.

The first request has started but has not yet finished when the retry arrives.

If both independently decide that the operation has not been seen before, both may continue and idempotency is lost.

The point that establishes the first execution must therefore be protected against concurrent access.

Database Uniqueness Provides the Final Duplicate Guard

PHP - Simplified uniqueness constraint
Schema::create(    'processed_operations',    function (Blueprint $table): void {        $table->id();         $table->string(            'operation_identifier',        );         $table->unique(            'operation_identifier',        );    });

An application-level check is useful, but two concurrent attempts may observe the same state at the same time.

For the fact that a logical operation has already been registered, I therefore want a database-level guarantee as well.

A uniqueness constraint does not define the full retry behavior, but it prevents two concurrent requests from both becoming the first execution.

A Retry Needs a Stable Result, Not Re-Execution

After successful completion, I keep enough information to ensure that a later retry does not execute the operation again.

This does not necessarily mean storing the raw HTTP response exactly as it was sent.

The system may persist a stable operation result and reconstruct the response from that.

The important part is that a lost response does not cause another side effect.

Idempotency Must Cover Side Effects, Not Only the Main Record

The main database record can be protected correctly while another duplicate still appears elsewhere.

For example, the retry may avoid creating a second entity but still send a notification twice or call an external system again.

I therefore look at the whole operation:

  • database state changes;
  • events that are produced;
  • external calls;
  • messages or notifications.

A safe retry means that the important outcome of the operation is not duplicated.

Not Every Request Needs a Dedicated Idempotency Mechanism

I do not add a dedicated idempotency mechanism to every endpoint automatically.

For ordinary reads, retries are often already safe.

It matters most for operations where re-execution creates another side effect and where a client may realistically retry after an uncertain outcome.

I therefore start not with the pattern, but with one question: what happens if the response is lost after successful execution?

For More Complex Cases I Track Operation State

The decision depends on the known operation state
Unknown operation        |        vaccepted        |        vin progress        |        +--> success        |        +--> controlled failure  Retry: success

Not every project requires this.

But the model is useful: a retry of a completed operation, a retry while execution is still in progress, and a genuinely new operation are three different cases.

If they are not distinguished explicitly, the code quickly turns into a collection of unrelated checks.

What I Verify in Tests

Core scenarios
1. First attempt   -> operation executes once  2. Retry after success   -> no new operation is created   -> previous result is returned  3. Two concurrent attempts   -> only one becomes the first execution  4. Same identity with different input   -> request is rejected 

The final test is especially important to me.

Idempotency should not turn identical input into a permanent ban on repeating a legitimate action.

If the user intentionally starts a new operation, it should remain new even when all of its values match a previous operation.

What I Check Before Adding Idempotency

Before implementing idempotency, I ask several questions:

  • Can the client lose certainty about the outcome after a response failure?
  • Is it safe to execute the operation twice?
  • What identifies one logical operation?
  • How is a retry distinguished from new intent?
  • What happens if two attempts arrive concurrently?
  • Is uniqueness protected at the database level?
  • What should a retry receive after successful completion?
  • Are all important side effects protected from duplication?

Idempotency Protects Intent, Not Just an HTTP Request

For me, the most important part of idempotency is not the retry mechanism itself.

The key is separating a logical operation from an individual attempt to deliver it to the server.

A lost response may force the client to retry. That is normal.

The server should recognize that the intent has not changed, avoid creating another side effect, and return a stable result.

When the user genuinely starts another action, even with identical data, the system should allow a new operation.

A network attempt may be repeated. That should not automatically create another business operation.

Idempotency is not about forbidding retries. It is about making sure a retry of the same operation remains the same operation.