Back to BlogTroubleshooting

The Checkout Bugs Quietly Killing Your E-Commerce Sales

Rupak Amin

Founder & Lead Engineer, RAITHub

8 min read

Most checkout bugs throw no visible error; they just lose the sale. The common ones: a payment that charges twice, a spinner that never resolves after the card is approved, a webhook that never fires so the order is paid but never created, oversold stock, and a layout that breaks on mobile. Each is invisible in your dashboard. Test them with payments in test mode and replayed webhooks.

If you would rather have your checkout audited and fixed, see how RAITHub would test this below. First, the bugs and how to find them.

Why are checkout bugs so easy to miss?

Because the people who build and demo a store check out successfully, with a good card, a fast connection and a modern phone. Real buyers arrive with declined cards, flaky mobile networks, two tabs open, autofill that fills the wrong field, and a back button pressed at the worst moment. The error happens on their side, so your logs look clean while your conversion quietly drops. The Baymard Institute's long-running research puts the average documented cart-abandonment rate at around 70% across many studies, with a large share of it down to checkout friction and process problems rather than price alone (Baymard Institute, cart abandonment rate statistics).

Speed is part of it too. Google's web.dev guidance on Core Web Vitals sets Largest Contentful Paint at 2.5 seconds or less and Interaction to Next Paint at 200 milliseconds or less as the "good" thresholds (web.dev, Web Vitals). A checkout that misses those loses buyers before they ever see a bug.

Which checkout bugs cost the most, and what do they look like?

BugWhat the buyer seesWhat it costs you
Double chargeCard debited twice after a retry or double-clickRefunds, chargebacks, a lost customer and support time
Stuck spinner after approvalPayment approved, page never confirms; they close the tabA paid order the buyer thinks failed; a duplicate attempt
Webhook never firesNothing; the order never appearsMoney taken, no order created, no fulfilment, an angry email
Oversold stockOrder confirmed for an item that is goneCancellations, refunds and a reputation hit
Mobile layout breakPay button off-screen or hidden by the keyboardSilent abandonment on the majority of traffic
Tax or shipping jumpTotal changes at the last stepA spike in abandonment at the final click

How do you stop double charges and stuck spinners?

Make the payment request idempotent and disable the button after the first click. An idempotency key lets a retried request return the original result instead of charging again; Stripe's API supports an Idempotency-Key header for exactly this, so a repeat with the same key returns the first response rather than creating a second charge (Stripe API, idempotent requests). The deeper pattern is in idempotency in API design.

Test it by firing the same payment twice and asserting one charge:

// tests/e2e/checkout-double-click.spec.ts
import { test, expect } from '@playwright/test'

test('a double-clicked pay button charges once', async ({ page }) => {
  await page.goto('/checkout')
  // ... fill test card details ...
  const pay = page.getByRole('button', { name: 'Pay now' })
  await Promise.all([pay.click(), pay.click()]) // simulate a frantic double-click
  await expect(page.getByText('Order confirmed')).toBeVisible()
  const charges = await test.step('count charges', async () => fetchTestCharges())
  expect(charges.length).toBe(1)
})

How do you test the webhook that creates the order?

This is the bug that takes money and creates no order. If you create the order only when the payment webhook arrives, a missed, delayed or unverified webhook means a paid-but-invisible order. Replay signed webhook events in test mode and assert the order is created exactly once, even when the same event is delivered twice. The full method, including why a webhook can return 200 and still not update anything, is in testing payments and webhooks.

// tests/api/checkout-webhook.spec.ts
import { test, expect } from '@playwright/test'
import { signedEvent, countOrders } from './helpers'

test('a replayed payment event creates one order', async ({ request }) => {
  const event = signedEvent('payment_intent.succeeded', { intent: 'pi_123' })
  await request.post('/api/webhooks/stripe', { data: event.body, headers: event.headers })
  await request.post('/api/webhooks/stripe', { data: event.body, headers: event.headers })
  expect(await countOrders('pi_123')).toBe(1)
})

Pair this with a reconciliation job that flags any successful payment with no matching order, so a webhook that silently stops firing is caught within minutes, not when a customer complains.

How do you stop a confirmed order from overselling stock?

Decrement stock inside the same database transaction that creates the order, and lock the row so two simultaneous orders cannot both pass the check. The reservation pattern, with the SQL, is in stopping overselling with stock reservation. Test it by placing two orders for the last unit at the same time and asserting that exactly one succeeds.

Buy, build or hire?

OptionWhat you getChoose this when
Hosted checkout from your platformPayments, retries and idempotency handled for youA standard store on a mainstream platform; test your theme and webhooks, not the gateway
A one-off checkout auditA ranked bug report across the paths aboveSales are leaking and you want to know where before you spend on fixes
Fix it with your own teamFull control, if you have the test setupYou can reproduce payment paths in test mode and have time to cover retries and webhooks
A managed QA planThe checkout suite built and retested on every releaseYou ship often and every deploy risks breaking the money path again

How long does it take to find and fix these yourself?

A focused checkout audit is usually a few days to reproduce the silent bugs in test mode and write the double-charge, webhook and oversell tests. Fixes vary: a disabled button is an hour; a missing idempotency key or a webhook that creates orders unreliably is a day or two each. The main risk of doing it yourself is declaring the checkout fixed after one clean manual pass, while the retry and webhook paths that lose real money stay untested. Make every fix land with a test, so the next deploy cannot undo it.

Why RAITHub for this

RAITHub built and runs TheSkinProof, the founder's own marketplace, with per-variant stock decremented inside the order transaction and four payment rails, and 750+ tests. On PropDesk, 1,024 tests cover Stripe rent collection, including the webhook and retry paths that break checkouts. The checkout audit and the retests are part of the QA as a Service offer, and if you want the issues fixed, the same engineers build software. See the eCommerce industry page for the wider context.

When you don't need us

  • You use fully hosted checkout and have not customised it. The gateway's own tests cover the money path; check your webhooks and theme.
  • Your checkout already has tests for retries, webhooks and overselling. Keep adding one with every bug fix.
  • You need a certified PCI assessment. That is a separate, certified engagement; RAITHub's security testing issues no attestation.

How RAITHub would test this

  • Scope: reproduce the silent checkout bugs in test mode; write tests for double charges, stuck confirmations, webhook idempotency and oversold stock; add a reconciliation check for paid-but-missing orders; run mobile and performance smoke checks on the checkout.
  • Ways to buy it: a fixed-price checkout audit with a ranked bug report, then a monthly QA plan or a dedicated QA team RAITHub manages and bills monthly. RAITHub does not place testers under your management.
  • Timeline: fixed in the written quote after the audit, based on your payment provider and how custom the checkout is.
  • What you receive: a ranked bug report, tests and CI in your repository, a fixed quote to fix the issues if you want RAITHub to, IP assigned to you and an NDA as standard.
  • Next step: a free 15-minute technical audit, then a fixed written quote.

See the app testing service, read about a full SaaS build, or book the free 15-minute audit.

Documentation checked on 10 October 2026.

Frequently asked questions

Why do I lose sales at checkout without seeing any errors?

Most checkout failures happen on the buyer's side: a declined card, a flaky mobile network, a double-click or a stuck confirmation. Your server logs look clean because the request never completed, so the only signal is a conversion rate that drops without an obvious cause.

How do I stop customers being charged twice?

Disable the pay button after the first click and send an idempotency key with the payment request, so a retried request returns the original result instead of creating a second charge. Then test it by firing the same payment twice and asserting one charge.

Why is my order paid but not created?

Usually the order is created only when a payment webhook arrives, and the webhook was missed, delayed or failed signature verification. Replay signed events in test mode, assert the order is created exactly once, and run a reconciliation job that flags any paid payment with no matching order.

How fast should a checkout be?

Google's web.dev thresholds for a good experience are Largest Contentful Paint at 2.5 seconds or less and Interaction to Next Paint at 200 milliseconds or less. A checkout slower than that loses buyers before they reach any functional bug.

Can RAITHub fix the checkout bugs it finds?

Yes. The engineers who test also build software, so RAITHub can fix the issues under a separate fixed quote, each fix landing with a test so the next deploy cannot undo it. Or your own team can fix from the ranked report.

checkout bugsecommerce checkout testingcart abandonmentstripe webhook testingdouble charge bugcheckout conversion

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.