

The Bug That Happens When Users Click Twice: Why Idempotency Matters in QA
A practical QA look at idempotency, duplicate submissions, retry-safe APIs, payment flows, webhooks and why testing the same action twice can reveal serious product risks.
Most software teams test the happy path.
A user clicks once. The request succeeds. The order is created. The payment is captured. The status changes. Everyone moves on.
But real users do not behave like perfect test cases. They double-click buttons. They refresh pages. Their internet drops. The browser freezes. A mobile app retries a request. A payment provider sends the same webhook twice. A server times out after completing the action, so the frontend thinks nothing happened and sends the request again.
That is where idempotency matters.
Idempotency is one of those words that sounds more complicated than it needs to be. In simple terms, it means that repeating the same operation should not create a new unintended result every time.
If a user submits the same payment request twice, they should not be charged twice. If a webhook arrives twice, the system should not create two orders. If a contract creation request is retried after a timeout, the product should not create two contracts.
Same action. Same intended result. No duplicate damage.
What idempotency means in practical terms
The HTTP specification describes an idempotent method as one where multiple identical requests have the same intended effect on the server as one request. In normal API design, GET, PUT and DELETE are usually expected to be idempotent by nature, while POST often needs extra protection when it creates or changes something important.
The product-quality definition is simpler:
If the same action happens twice by accident,
the user should not pay for that accident.For example, creating an order once and then retrying the same operation after a lost response should not create a second order. The system should either return the original order or safely explain that the operation has already been processed.
In many products, this looks like a small edge case until it touches money, contracts, bookings, balances, subscriptions or permissions. Then it becomes support tickets, refunds, reconciliation work, angry customers and trust damage.
The problem is not only double-clicking
The obvious example is the user who clicks a button twice. That is real and should be tested. But idempotency problems are usually bigger than that.
Duplicate actions can also happen because of slow networks, browser refreshes, two open tabs, frontend retries after timeout, mobile background retries, backend job retries, duplicate webhook delivery, queue reprocessing, third-party callbacks or a crash that interrupts the flow halfway.
Stripe supports idempotency so clients can retry requests safely without accidentally performing the same operation twice. PayPal describes the same risk from another angle: a payment capture request can time out from the client side while still being completed on the server, so retrying needs a way to avoid duplicate capture.
That is the core issue. The frontend may not know what happened. The user may not know what happened. Even the calling service may not know what happened. But the system still needs to behave safely.
The dangerous state: did it go through?
One of the most dangerous moments in software is when the action succeeded, but the response failed.
For example:
User clicks Pay.
Backend captures payment.
Response to frontend is lost.
Frontend shows loading or error.
User clicks Pay again.From the user’s point of view, the payment may look incomplete. From the system’s point of view, the money may already be captured.
Without idempotency, the second attempt may become a second payment.
This is why disabling the button after one click is useful, but not enough. It improves the UI and reduces accidental repeated submissions, but it does not protect the backend from repeated API requests, two browser tabs, webhook retries, mobile retries or someone replaying the request from DevTools or Postman.
Frontend protection is good UX. Backend idempotency is product protection.
Duplicate transactions are not theoretical
Duplicate-payment problems happen in real financial systems.
TIME reported that Bank of America refunded Apple Pay users after around 1,000 transactions were affected by duplicate charges. ABC reported a Commonwealth Bank issue where customers were encouraged to check statements after duplicate transactions appeared. CNA reported that 375 OCBC customers were charged twice for AXS payments due to a processing issue. WIRED reported an older Wal-Mart incident where more than 800,000 credit and debit card transactions were double- or triple-billed.
These examples are not all necessarily idempotency bugs in the narrow code-level sense. Some were described as glitches, vendor processing issues, hardware problems or payment-processing failures.
But from a QA perspective, they show the same product risk:
A payment action was represented more than once.
Customers saw duplicate financial impact.
The business had to reverse, reconcile and explain it.When duplicate processing happens in a financial flow, the bug is not just technical. Customers do not experience it as a retry-safety issue. They experience it as money missing from their account.
Where idempotency bugs usually hide
Idempotency problems often hide in flows that look normal when tested once.
A QA engineer may create an account, place an order, pay an invoice, upload a document, send a message, confirm a contract or trigger a notification and see everything pass.
The issue appears when the same action is repeated under pressure.
Common risky areas include payment capture, refunds, withdrawals, deposits, checkout submission, order creation, contract creation, booking confirmation, account registration, password reset requests, file uploads, message sending, notifications, subscription activation, webhook processing, balance updates, reward allocation and admin state transitions.
The pattern is usually the same: the first request creates a side effect, and the second request should be recognised as the same operation. If it is not, the product creates duplicate state.
Example: duplicate contract creation
Imagine a lending or marketplace platform where a user accepts an offer.
The happy path looks fine: the user clicks Accept offer, a contract is created, and the user is redirected to the contract page.
Now test the real world:
User clicks Accept offer.
Network is slow.
User clicks again.
Frontend sends two requests.
Backend creates two contracts.The UI may only show one of them. The user may not notice immediately. But the backend now has two active records, and maybe both require payment, collateral, confirmation or cancellation.
That is not only a button bug. That is a business-logic bug.
A safer system would use an idempotency key or unique business constraint so the backend understands that this user accepting this offer in this context is one intended operation.
Example: webhook processed twice
Webhooks are another classic source of idempotency problems.
Payment providers and external services often retry webhook delivery because they cannot always know whether your system received the event. That is normal. It is also dangerous if the receiving system treats every webhook delivery as a brand-new event.
A safer webhook flow stores and checks the provider event ID:
First webhook: event_id = evt_001
→ process event once
→ store event as processed
Repeated webhook: event_id = evt_001
→ return success
→ no duplicate side effectThis matters because webhook retries are part of distributed systems. A webhook receiver should assume the same event may arrive again.
For QA, webhook testing should not stop at “event received successfully.” It should also ask what happens if the same event is delivered twice.
Idempotency keys are useful, but not magic
Idempotency keys are useful, but they are not magic.
A good implementation still needs to answer practical questions. Who generates the key? How long is it stored? Which operation does it represent? What happens if the same key is reused with a different payload? What happens if two identical requests arrive at the same time? What response does the duplicate request receive?
Stripe compares incoming parameters with the original request and errors if they do not match, helping prevent accidental misuse of a key. PayPal also notes that not every API supports its idempotency header, and that simultaneous requests with the same request ID may not both be processed in the same way.
One weak pattern is generating a new idempotency key every time the user clicks submit. That does not protect against double-clicks, because each click becomes a new operation.
A better pattern is to generate one idempotency key for one intended operation. Retries of that same operation reuse the same key. A new key is generated only when the user intentionally starts a new operation.
Even then, backend business rules still matter. One invoice should only be paid once. One webhook event ID should only be processed once. One refund request should only create one refund for the same captured payment and amount. Idempotency keys are one layer. Business rules are another. Good systems usually need both.
QA should test the retry, not only the request
The basic happy-path API test is not enough.
For idempotency, the better test is to send the same request, interrupt or lose the response, retry the same request and check that only one side effect exists.
This is where DevTools, Postman, API logs, admin panels and database checks become useful.
A practical QA flow could be:
1. Start the action in the UI.
2. Capture the API request.
3. Replay the same request.
4. Try the same action from two tabs.
5. Refresh during loading.
6. Check backend state and side effects.The important part is not only whether the second request returns an error. The important part is whether the system state remains correct.
A duplicate request may safely return an existing result, the original operation result, a clear conflict response or a validation response saying the operation is no longer valid. The exact status depends on the product and API design. But it should not create the same side effect twice.
What good behaviour looks like
For a payment flow, good idempotent behaviour means the first request captures the payment once, and a retry with the same operation key does not create another capture.
For an order flow, the first request creates the order, and a retry with the same operation ID returns the existing order instead of creating another one.
For a webhook, the first delivery processes the event, and repeated delivery of the same provider event ID returns success without duplicating emails, credits, ledger entries or state transitions.
This is the behaviour QA should be looking for.
What bad behaviour looks like
Bad behaviour usually looks normal until you compare records.
Examples include two payments with the same amount and user within seconds, two orders from one checkout attempt, two contracts from one offer acceptance, two confirmation emails, two refunds, two balance updates, duplicate webhook processing or repeated audit-log entries for the same state transition.
Sometimes the UI hides the problem. A product may only display the latest record, while the backend contains duplicates. Or the user may only see one confirmation page, while emails, ledgers, admin records or payment provider dashboards show two actions.
That is why idempotency testing should not rely only on the screen. QA needs to check the connected systems around the action.
Why idempotency is a product-quality issue
Idempotency bugs are not always dramatic in the beginning.
A duplicate email is annoying. A duplicate notification is messy. A duplicate file upload wastes storage. A duplicate support ticket creates noise.
But the same weakness in a financial or transactional flow can become serious quickly.
A duplicate payment can remove money from a customer’s account. A duplicate refund can create a financial loss for the business. A duplicate withdrawal can become a security and reconciliation issue. A duplicate contract can create legal, operational or support confusion. A duplicate ledger entry can break reporting.
This is why idempotency belongs in product quality, not just backend architecture. It affects user trust. And trust is hard to repair after money appears to move incorrectly.
Where frontend testing ends and backend validation begins
A common mistake is to treat duplicate submission as only a frontend problem.
The team disables the button after the first click and considers the issue solved.
That is useful, but incomplete.
Frontend checks can reduce accidental user behaviour. Backend checks protect the product when the frontend is bypassed, delayed, repeated or wrong.
A good QA question is:
If I bypass the UI and send this request twice,
does the backend still protect the user and the system?That question often finds stronger bugs than normal UI testing.
This is exactly where QA, API testing and product-quality review overlap.
How to report an idempotency bug
A good idempotency bug report should be very clear because developers need to understand whether the problem is in the frontend, backend, API design, business rule, queue processing or third-party integration.
Useful evidence includes the endpoint, payload, idempotency key or missing key, request timestamps, response bodies, created object IDs, user account, related records, screenshots and the final backend state.
The strongest bug reports do not only say “I clicked twice and something weird happened.” They show the system effect.
For example: “Two payment captures were created for the same user, amount and invoice after the capture request was retried during a timeout. Expected result: only one payment capture should exist for the intended operation.”
That gives the team the technical evidence and the product impact in one report.
How teams can design safer flows
From a QA perspective, idempotency should be discussed before implementation is finished, not only after a duplicate bug appears.
Useful design decisions include using idempotency keys for mutating API requests, reusing the same key for retries of the same operation, rejecting the same key with a different payload, storing processed webhook event IDs, protecting business entities with unique constraints, guarding in-progress operations and making retry behaviour visible in logs.
Teams should also make status transitions one-way where appropriate, return the existing result for safe duplicates, fail loudly when duplicate intent is unclear and test retry behaviour under slow network and timeout conditions.
The practical QA point is simple: do not assume the protection works. Test it.
Where Laidoner Solutions helps
Laidoner Solutions helps software teams catch issues before users do.
Idempotency is a good example of the kind of risk that can be missed when testing stays too close to the happy path. The UI may work. The button may submit. The success message may appear. But the product may still be vulnerable to duplicate actions when real-world behaviour gets messy.
This is where practical QA, API validation and product-quality review help.
The work can include testing duplicate submissions, checking API retry behaviour, validating payment and contract flows, replaying requests from DevTools or Postman, checking webhook duplication handling, reviewing state transitions, testing refresh and timeout scenarios, checking frontend and backend protection separately and documenting clear defect reports with request evidence.
The goal is not only to find bugs. The goal is to find the kind of bugs that create support work, financial mistakes, broken trust and avoidable release risk.
Final thoughts
Idempotency is not just a backend detail.
It is a product-quality safeguard.
Most users will never know the word. They do not care whether the system used an idempotency key, a unique constraint, a webhook event table or a retry-safe operation design.
They care that clicking twice does not charge them twice. They care that refreshing a page does not create another order. They care that a timeout does not leave them unsure whether money moved.
That is why QA should test beyond the perfect path.
The important question is not only: does the action work once?
The better question is: what happens if the same action happens again by accident?
In simple products, the answer may be a small annoyance. In financial products, the answer can be duplicate charges, refunds, reconciliation work and lost trust.
Sometimes the most expensive bug is not the action that fails. It is the action that succeeds twice.
Sources and further reading
- RFC 9110 — HTTP Semantics
- Stripe API Documentation — Idempotent Requests
- Stripe Engineering — Designing Robust and Predictable APIs with Idempotency
- PayPal Developer Documentation — Idempotency
- AWS Builders Library — Making Retries Safe with Idempotent APIs
- TIME — Bank of America Issues Refunds to Apple Pay Users
- ABC News — Commonwealth Bank Duplicate Transactions
- CNA — 375 OCBC Customers Charged Twice for AXS Transactions
- WIRED — Wal-Mart Double and Triple Billing Incident
Need practical QA support?
Laidoner Solutions helps software teams with manual QA, API testing, localization review, release checks and clear defect reporting.
Contact Us