Retries are one of those things that look harmless until the first time they duplicate a real business operation.
What if the application already created the order, reserved the stock, sent the message, or called a payment provider — and only the response was lost?
Send the same request again and you can get the worst kind of bug: one that is technically understandable, difficult to reproduce, and very expensive in production.
This is the problem that pushed me to build HttpIdempotencyBundle, a small Symfony bundle for explicit HTTP request idempotency.
The interesting part is everything that has to be true before we can safely say:
"This request is a retry of the same operation, so we should not execute it again."
And yet, unless we have another mechanism in place, we may now create order #743 as well.
The client generates a unique value for one logical operation and sends it with every retry:
What should happen if a client accidentally reuses the same key for a different request?
If we only store the key, the second request might receive the response from the first one.
The server must therefore know not only which key was used, but also which request that key belongs to.
For HttpIdempotencyBundle, the default fingerprint includes the important parts of the request, such as: HTTP method logical operation path normalized query parameters normalized Content-Type raw request body
Idempotency should protect us from duplicate execution, not hide application bugs.
If you run multiple PHP workers, containers, or application nodes, a local in-memory cache or a local filesystem lock cannot coordinate all of them.
Every application instance participating in the same operation must see the same idempotency state and the same locking mechanism.
Suppose request A and request B both perform their first read before either one obtains the lock.
It executes the controller, stores the completed response, and releases the lock.
That second read closes the race between the initial lookup and lock acquisition.
