Founder & Lead Engineer, RAITHub
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?
| Bug | What the buyer sees | What it costs you |
|---|---|---|
| Double charge | Card debited twice after a retry or double-click | Refunds, chargebacks, a lost customer and support time |
| Stuck spinner after approval | Payment approved, page never confirms; they close the tab | A paid order the buyer thinks failed; a duplicate attempt |
| Webhook never fires | Nothing; the order never appears | Money taken, no order created, no fulfilment, an angry email |
| Oversold stock | Order confirmed for an item that is gone | Cancellations, refunds and a reputation hit |
| Mobile layout break | Pay button off-screen or hidden by the keyboard | Silent abandonment on the majority of traffic |
| Tax or shipping jump | Total changes at the last step | A 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?
| Option | What you get | Choose this when |
|---|---|---|
| Hosted checkout from your platform | Payments, retries and idempotency handled for you | A standard store on a mainstream platform; test your theme and webhooks, not the gateway |
| A one-off checkout audit | A ranked bug report across the paths above | Sales are leaking and you want to know where before you spend on fixes |
| Fix it with your own team | Full control, if you have the test setup | You can reproduce payment paths in test mode and have time to cover retries and webhooks |
| A managed QA plan | The checkout suite built and retested on every release | You 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.
Related posts
Ready to discuss your project?
Book a free 15-minute technical audit with our engineering team.