A timeout does not indicate that an operation has failed: it only confirms that the client did not receive a response within the allotted time. The server may have created the order, the payment provider may have accepted the charge, or an asynchronous process may still be running. If the client retries without control, a single business intent can produce duplicate effects.
Idempotency in PHP turns a technical repetition into a lookup or the return of the result already obtained. It does not mean ignoring all duplicates or relying solely on the user not clicking twice. It is an explicit contract between the client, API, persistence layer, and, where applicable, external systems.
The problem: the response is lost, but the effect remains

Consider an endpoint that confirms a purchase. The application validates the request, records the order, requests the charge, and prepares a response. The connection drops just before the client receives it. When the same form is resubmitted, the endpoint cannot infer from the content that it is the same purchase: two orders with the same products may be valid and distinct intents.
The problem also arises in user registrations, credit allocation, document issuance, synchronizations, webhooks, and administrative actions. There are three elements that should be kept separate:
- Business intent: “I want to confirm this specific purchase.”
- Technical request: an HTTP submission with headers, body, and authentication context.
- Execution attempt: each internal processing run, queue retry, or provider call.
The idempotency key identifies the intent, not an HTTP connection or each server attempt. Therefore, it must survive network retries and, when the flow requires it, process restarts.
Which operations need idempotency and which do not
Prioritize operations that create, confirm, charge, send, reserve, notify, or modify a resource with relevant consequences. A POST /payments, order confirmation, or webhook receipt are clear candidates. So is a queue job that can be delivered more than once.
A pure read normally does not need an idempotency key. An update can have different semantics: setting a desired state, such as PUT /profiles/42, can be idempotent by design if the same representation leaves the resource unchanged. By contrast, an action such as “add balance” is not idempotent merely because it uses a particular verb.
A key should not be used as a substitute for other rules either. To prevent two compatible reservations in limited inventory, you need domain invariants, concurrency control, and a reservation policy. To run a task only once in a distributed environment, actual delivery is often at least once; the consumer must tolerate duplicates.
Key and persistent record design
The client should generate an opaque and sufficiently unpredictable key when the business intent arises, retain it for as long as it may retry, and send it, for example, in Idempotency-Key. If the server generates it on each receipt, it will not be able to link a later repetition. In internal flows, the key can be derived from a stable business event identifier.
Its scope should include the actor or tenant and the operation. The same string should not collide between two accounts or between “create order” and “issue refund.” Define a retention period aligned with the actual retry window and domain risks. Deleting the record too early reopens the door to duplicates; retaining it indefinitely increases cost and requires a privacy and deletion policy.
A minimum persistence model includes:
- security or tenant scope, operation name, and idempotency key;
- cryptographic fingerprint of a normalized payload;
- status:
processing,completed,failed, orpendingwhen external confirmation is uncertain; - response code and body to be returned consistently;
- identifiers of the created resource, internal correlation, and external provider reference;
- creation, update, and expiration dates.
The fingerprint prevents an important error: reusing the same key with different data. In that situation, respond with a conflict and do not process the new payload. For the comparison to be reliable, normalize fields whose order has no meaning and exclude changing metadata that is not part of the intent.
PHP flow: reserve before producing the effect
The protection must be backed by a unique database constraint on the scope, operation, and key. Checking first and inserting afterward is not enough: two simultaneous requests can observe the absence of the record and proceed at the same time.
The recommended flow is to reserve atomically. If the insertion succeeds, that process is the initial owner of the execution. If there is a uniqueness conflict, read the existing record, verify the fingerprint, and act according to its status. A completed result returns exactly the persisted response; an operation in progress can return a pending status or wait only for a bounded interval before reading again.
begin transaction
insert idempotency_records(scope, operation, key, payload_hash, status)
values (?, 'create_order', ?, ?, 'processing')
-- the unique constraint determines the owner
commit
if reservation_was_created:
result = execute_business_operation()
persist_completed_response(result)
else:
record = load_existing_record()
assert_same_payload_hash(record)
return replay_or_pending(record)Do not keep a transaction or row lock open during a slow call to a provider. That reduces capacity and can cause prolonged blocking. Instead, reserve and confirm the local status in short transactions. If the external effect and the local record must be coordinated, also store a delivery order in a transactional table and process it separately. This pattern does not eliminate retries, but it makes it possible to recover pending work without losing the recorded intent.
Concurrency, timeouts, and uncertain states
Two requests with the same key can arrive milliseconds apart. The unique constraint determines which one reserves the operation. The second must not start another external effect. It can respond with 202 while the status is processing or pending, including an identifier to query the result; if the contract requires a synchronous response, it can wait for a limited time and reread the record.
A failure before starting any effect allows you to mark failed with a reproducible error. However, a timeout when calling an external system creates uncertainty: it is not correct to automatically mark it as failed or simply resend an order. Store the sent request reference if one exists, query the provider using that reference, and reconcile the result. Until there is confirmation, keep pending and communicate that the outcome is not yet final.
The external call also needs a stable reference. If the provider supports its own idempotency key, propagate a key associated with the same intent. If it does not, use merchant identifiers, subsequent reads, periodic reconciliation, and operational procedures for ambiguous cases. No local transaction can make a database write and an independent remote API atomic.
What an idempotency key does not solve
Idempotency prevents repetition of a recognized intent; it does not decide how to undo an irreversible effect. A physical shipment, an already settled transfer, or a notification seen by a user may require compensation, cancellation, or manual handling. Design those actions as explicit business processes, with permissions, states, and auditing.
Do not confuse a correction with a retry either. If the user changes the address, amount, or products after an error, there is a new intent and a new key must be used. Reusing the previous one with a different payload must result in a conflict, not silently update the original operation.
Testing, observability, and checklist

Test more than the happy path. Interrupt the response after persisting the result, repeat the same key in parallel, restart a worker after reserving the record, and simulate a timeout after sending an external request. Verify that only one business resource exists, that the repeated response retains the same result, and that a different payload with the same key is not accepted.
Log, without exposing sensitive data, the key or a derived secure identifier, the scope, status, correlation, and external reference. Metrics for key conflicts, operations pending for too long, and unresolved reconciliations help support and operations distinguish a normal retry from an incident.
- Does the key represent a business intent and have a defined scope?
- Is there a unique constraint that prevents two concurrent reservations?
- Is a payload fingerprint compared and are intent changes rejected?
- Is a response or result persisted so that it can be replayed consistently?
- Do uncertain states allow querying and reconciliation before retrying?
- Does every external effect have a reference, recovery path, and operational alternative?
- Have duplicates, failures, queue retries, and real concurrency been tested?
Applied this way, idempotency does not promise that a network is reliable. It makes inevitable failures have an outcome that is controllable, traceable, and consistent for the business.



