Why Orders Get Lost at Checkout: 7 Common Bugs.
- Published
- Updated
Orders get lost at checkout for a small number of recurring reasons: double clicks, lost responses from the payment provider, an expired session, overly strict field validation, failures in dependent services, price mismatches and mobile problems. Below are seven typical checkout bugs we find in online stores during testing, with steps to reproduce, the cause and the fix. The material is useful both for owners (to know what to ask a developer) and for developers (to check their own code).
A quick note: we do not cite other people's cases or statistics. Everything below is a typical scenario that is easy to reproduce on any store. We find these in our work as QA, and you should check for them before a customer finds them for you.
Why checkout is a high-risk zone
Checkout combines the most moving parts in the shortest time: a form with validation, total calculation, delivery choice, an external payment system, order creation in the database, emails and notifications. Any part can fail, and the customer sees nothing useful: they either get a confusing error or pay and never receive an order.
Two situations are especially dangerous:
- The customer paid, but there is no order. This is the worst scenario: the money is charged and the store knows nothing about it.
- The customer could not finish and left. You will never find out: there is nothing in the database, and analytics shows only a "bounce".
That is why checkout testing is needed not only for all the "happy" flows but also for abnormal ones: a slow network, double clicks, a closed tab, a bank decline. We collected the basic checks in the 60-point checklist, and here we go through the most expensive mistakes in detail.
Bug 1. A double click creates two orders (or none)
Symptom. The customer clicks "Confirm order" and the page thinks for a few seconds. They click again. The system ends up with two identical orders, or the second click triggers an error and the first order "hangs".
Steps to reproduce.
- Add a product to the cart and go to checkout.
- Turn on Slow 3G in DevTools.
- Click the confirm button twice quickly.
- Check the number of orders in the admin panel.
Cause. The button is not disabled after the first click, and the server does not check whether the request is a repeat.
Fix. Two layers of protection are needed. On the client, the button becomes inactive and shows an indicator. On the server, an idempotency key is added: the client generates a unique token when the form opens and sends it with the request, and the server rejects a repeated request with the same token.
// Laravel: protect against a duplicate order with an idempotency key
$order = Order::firstOrCreate(
['idempotency_key' => $request->input('idempotency_key')],
$orderData
);
if (! $order->wasRecentlyCreated) {
return response()->json(['order_id' => $order->id]); // same result
}
For this the orders table needs a unique index on idempotency_key, otherwise two nearly simultaneous requests will still race and create a duplicate.
Bug 2. The payment provider reported payment, but the order did not update
Symptom. The customer paid by card and the bank charged the money, but the order stayed in the "awaiting payment" status. The manager does not ship the goods, and the customer calls furious.
Steps to reproduce.
- Place an order with card payment in the sandbox.
- Close the tab right after payment, without returning to the site.
- Check the order status.
Cause. The store updates the status only when the customer returns to the "Thank you" page. But the customer can close the tab or lose connection. The payment provider sends a separate server-to-server message (a callback, or webhook), and that must be the source of truth.
Fix.
- The payment status changes only on a server callback verified by signature.
- The customer's return to the site only displays the state and does not change it.
- The callback must be idempotent: the provider may send it twice, and the second handling must neither duplicate emails nor change the status back.
- For orders that sit in "awaiting" for a long time, a background job is needed that asks the provider for the status directly.
Payment and delivery integrations are a big topic of their own, covered in Nova Poshta and Payment Integration: Where It Breaks Most Often.
Bug 3. The session expired and the cart disappeared
Symptom. The customer fills in the form for 15 minutes, gets distracted, clicks "Place order" and instead of an order sees "419 Page Expired" or an empty cart.
Steps to reproduce.
- Add a product to the cart and open checkout.
- Leave the page for longer than the session lifetime.
- Try to submit the form.
Cause. The session (or CSRF token) has expired, and the site responds with a dry technical error and does not keep the entered data. It happens especially often on mobile: the browser unloads the tab from memory while the customer switches to the banking app.
Fix.
- The cart is stored in the database (tied to a guest identifier in a cookie), not only in the session.
- The protection token refreshes automatically, and on error the user sees a clear message with the form data preserved.
- Entered fields are temporarily stored on the client (without passwords or card data) and restored after a reload.
Bug 4. Validation rejects normal data
Symptom. The customer enters correct data, but the form says "invalid format" and will not let them continue. They leave, and you never even see that the attempt happened.
Typical cases.
- A phone number in the format
+38 (050) 123-45-67against a mask that expects digits only. - An email with a trailing space, added by smartphone autofill.
- A surname with an apostrophe (
O'Brien) or a double-barrelled name with a hyphen. - An address with Cyrillic and Latin letters in the street name.
- A postcode or house number with a letter (
12/A).
Steps to reproduce. Make a small list of problem values and enter them into each field in turn. Check browser autofill on a phone separately.
Fix. Normalize input instead of rejecting it: strip spaces from email, reduce the phone to a single format on the server, do not restrict characters in names. The rule: better to accept too much and verify afterwards than to turn a customer away. The error should appear next to the specific field and explain what is wrong.
Bug 5. Nova Poshta: an empty or unavailable branch selector
Symptom. The customer chooses "Nova Poshta", but the branch list does not load, or the city is found but there are no branches, or the selected branch disappears after the item quantity changes.
Steps to reproduce.
- Choose Nova Poshta delivery.
- Search for a city by part of its name, and also for a settlement that has namesakes in other regions.
- Select a branch, then change the product quantity and see whether the selection resets.
- Turn off the network in the middle of selection and try again.
Cause. The store depends on an external API and has no fallback. If the API is slow or unavailable, the form freezes. Another variant: the branch list is cached for too long, and the customer is offered a closed branch.
Fix.
- A timeout and retries for API requests, and a clear message on failure.
- A cache of the city and branch directory with regular background refresh.
- Preserving the selection when the cart changes.
- A fallback: a "manual address" field or the option to place the order with delivery clarified by a manager, so another party's API outage does not block a sale.
Bug 6. The checkout price differs from the catalog price
Symptom. In the catalog the product costs UAH 1,200, in the cart 1,250, at checkout 1,190. The customer gets confused and leaves, or worse: buys at the wrong price, and you work at a loss.
Causes.
- A discount is calculated at different stages with different formulas (before or after delivery).
- Rounding happens in different places, and the cents drift apart.
- The catalog cache shows the old price after a change.
- A promo code applies to products that should not be part of the promotion.
- Currency or VAT is calculated differently on the frontend and the backend.
Steps to reproduce. Take 3–4 products with different discounts and compare the price in the listing, the product page, the cart, checkout, the email and the admin panel. Do this with and without a promo code. Verify each calculation with a calculator.
Fix. A single place where the price is calculated. The frontend only shows what the server returned. The order total is fixed at the moment of creation (a price snapshot), so later catalog changes do not affect orders already placed. Money is stored in minor units as an integer, not as a floating-point number:
// risky: float
$total = 19.99 * 3; // 59.97, but 59.969999... in some cases
// reliable: minor units as integers
$totalMinor = 1999 * 3; // 5997
For a store this matters especially, because rounding accumulates on large orders and shows up as "it somehow doesn't match the accounting".
Bug 7. A mobile checkout where the button cannot be tapped
Symptom. On an iPhone or Android phone the customer cannot complete the order: the button is covered by a sticky banner, the keyboard hides the field, the dropdown will not open, the form "jumps".
Steps to reproduce.
- Open checkout on a real phone (an iPhone with Safari and an Android with Chrome), not only in an emulator.
- Fill in the form, switching between fields.
- See whether anything covers the confirm button when the keyboard is open.
- Try rotating the screen and changing the system text size.
Cause. The layout was checked on desktop. Mobile browsers handle screen height, fixed elements and input autozoom differently. In Safari a field with a font size below 16 pixels zooms in automatically on focus, which breaks the layout.
Fix. A field font size of 16 pixels or more, the right field types (type="tel", inputmode, autocomplete), a completion button that is not fixed where the keyboard can cover it, and checking on real devices. In most stores mobile traffic makes up the majority of visits, so this bug is especially expensive.
Want to know where orders get lost in your own store? Our free 48-hour express check will show 10–15 key problems: we check checkout, the mobile version and speed. If there are many problems, we will offer an ecommerce QA audit from UAH 6,000.
How to find such bugs before customers do
One thing unites all seven bugs: they are invisible on the "happy path". A developer went through checkout once, everything worked, and the task was closed. To catch these problems systematically you need an approach.
Test negative scenarios. For every step, ask: "What happens if this does not work?" A payment error, no network, a double click, a session timeout, a repeated request.
Use the services' test environments. All popular payment providers and Nova Poshta have a sandbox. A test card with successful payment, a decline and a 3-D Secure failure is the minimum.
Log and monitor. If orders get lost, you should know before the customer's call. Set up:
- recording of all payment provider callbacks with body and signature;
- alerts (to Telegram or email) for orders that sit in "awaiting" for more than 30 minutes;
- funnel metrics: how many people reached checkout and how many completed it. A sharp drop is a signal.
Automate the critical path. The most important scenario, "product → cart → payment → order", should be covered by an automated test that runs on every release. We use Playwright and run tests in CI; read more about test automation and what to automate first in Manual vs Automated Testing.
Repeat regression after changes. Every module update, pricing change or new delivery method can break what worked. Before a release, run the key checks, not only the new feature.
Short summary
Seven typical reasons orders get lost at checkout:
- A double click without idempotency.
- The payment status depends on the customer returning, not on the callback.
- Sessions and tokens expire without saving the cart.
- Validation rejects normal data.
- Dependence on the Nova Poshta API without a fallback.
- Price mismatches between stages.
- An unusable mobile checkout.
None of these bugs needs complex technology to fix, only attention to negative scenarios and testing on real devices. Their price is in direct losses: every lost order is not only a lost sale but a lost customer.
Start with two things today. First, go through checkout yourself on a phone with a weak connection, pressing buttons twice. Second, make sure you have alerts for orders that did not complete payment. It takes an hour and may save weeks of revenue.
Need a checkout review from people who do this every day? Describe your store in the brief or see how website and store testing works: regression, cross-browser checks, and a report with video and priorities.
Why did the customer pay but the order did not appear in the admin panel?
Most often the callback from the payment system never arrived or was not processed correctly, so the order status did not update. Check the callback logs and compare them with the payment provider statement. Callback handling must be reliable and safe against repeated calls.
How can I tell that orders are being lost at checkout?
Compare started and completed checkouts in your analytics and look at the server error log. A sharp drop at one step points to the problem. It also helps to place test orders yourself from different devices and with different payment methods, and to ask support about customer complaints.
Why can the checkout price differ from the catalog price?
Usually because of cached prices, different rounding, or discounts and coupons calculated in different places in the code. The final amount should come from a single server-side calculation. Check promotional items, coupons and quantity changes in the cart. Mismatches erode trust and cause drop-offs right before payment.
How often should checkout be tested?
After every release that touches the cart, payment, delivery or forms, and after CMS and plugin updates. It helps to keep a short regression set for the critical purchase path. If problems have piled up, a one-off QA audit from UAH 6,000 can help.