The shop was generating duplicate orders – a customer clicked “Order” once but two identical orders were created with double payment. The problem affected ~2% of orders, mainly on slow internet connections. Fix time: 6 hours.
Symptoms
- Admin panel shows pairs of identical orders seconds apart
- Customers complain about double card charges
- Problem more frequent on slow connections and mobile devices
- Logs show two POST requests to
/rest/V1/carts/mine/payment-informationat the same time
Cause
The “Order” button was not disabled after the first click. On slow connections users clicked again thinking nothing had happened. Two parallel requests reached the server – both succeeded because there was no idempotency mechanism.
Solution
<?php
class PlaceOrderIdempotencyPlugin
{
private const LOCK_TTL = 30;
public function __construct(
private \Magento\Framework\Cache\FrontendInterface $cache,
) {}
public function aroundSavePaymentInformationAndPlaceOrder(
\Magento\Checkout\Api\PaymentInformationManagementInterface $subject,
callable $proceed,
int $cartId,
\Magento\Quote\Api\Data\PaymentInterface $paymentMethod,
?\Magento\Quote\Api\Data\AddressInterface $billingAddress = null
): int {
$lockKey = 'order_lock_' . $cartId;
if ($this->cache->load($lockKey)) {
throw new \Magento\Framework\Exception\LocalizedException(
__('Your order is being processed. Please wait.')
);
}
$this->cache->save('1', $lockKey, [], self::LOCK_TTL);
try {
return $proceed($cartId, $paymentMethod, $billingAddress);
} finally {
$this->cache->remove($lockKey);
}
}
}
Takeaways
Any operation that should execute exactly once requires an idempotency mechanism. In checkout: disable button on the JS side + idempotency key on the server side. Redis with TTL is the ideal mechanism for short-lived locks.
