A client that times out cannot tell whether the request was processed. Retrying risks a double charge; not retrying risks losing the order. The only way out is for the server to recognise the repeat.
// client sends the same key on every retry of one logical operation
// Idempotency-Key: 8f14e45f-ea27-4d1b-9c0a-6f0e2b1a9c33
$existing = $this->idempotency->find($key);
if ($existing !== null) {
return $existing->response; // same answer, no second charge
}
$response = $this->process($request);
$this->idempotency->store($key, $response, 24 * 3600);
return $response;
The key is generated by the client and reused across retries — generating a new one per attempt defeats the whole mechanism. Storing the response rather than a flag matters: the retry must receive the same answer, not a 409. There is a race between the check and the store, so the insert should rely on a unique constraint rather than a prior read.