Why Did the User Get Charged Twice After Clicking Once?

The UI sent one request. A proxy timed out and retried it. Both requests reached the payment service.

A timeout is an unknown outcome

The absence of a response does not prove the operation failed. The server may have committed just before the connection disappeared.

Give retries an identity

The client sends an idempotency key representing the logical operation. The server stores that key with the outcome and returns the same outcome for repeats.

POST /payments HTTP/1.1
Idempotency-Key: checkout-84f33
Content-Type: application/json