The mobile bug we missed because we only tested on Wi-Fi
On office Wi-Fi the payment flow was flawless. On a train, it charged people twice. The bug lived in a network condition we never tested. Here's the case study.
This is a case study, details blurred, of a bug that was structurally invisible to us: it could only happen on a slow or flaky connection, and we did all our testing in an office on fast, stable Wi-Fi. The app worked perfectly under the one condition we tested, and broke under the conditions most of our users actually had.
Context
A mobile app with an in-app payment step: tap "Pay", a request goes to the backend, a spinner shows, success screen on completion. Tested thoroughly — on the office Wi-Fi, where every request came back in well under a second.
Symptoms
Support tickets about double charges. Customers swore they tapped Pay once; their statement showed two identical charges minutes apart. It was intermittent and we couldn't reproduce it at our desks — which, again, was the clue we didn't read at first.
Investigation
The pattern in the data: double charges clustered around users on cellular networks, and around times of day that suggested commutes. Reproducing it meant leaving the office (literally) — or throttling the connection to simulate a train. On a slow link, the sequence was:
- User taps Pay. Request goes out.
- The connection is slow; the app's request times out client-side after a few seconds and shows "Payment failed, please try again."
- But the request hadn't failed — it was just slow. It reached the backend and succeeded a moment later.
- The user, seeing "failed", taps Pay again. Second charge.
The backend happily processed both, because nothing told it they were the same intended payment.
Root cause
Two compounding issues, both hidden by fast Wi-Fi:
- A client timeout shorter than real-world latency. On Wi-Fi, responses were instant, so the timeout never fired. On cellular, it fired constantly — turning slow successes into displayed "failures."
- No idempotency on the payment. A retried payment created a second charge instead of being recognised as the same one. The classic idempotency gap from the 12 API bugs — invisible until something triggers a retry, and a flaky network triggers retries all day.
What the tests missed
We tested the payment flow's logic exhaustively and its network conditions not at all. Every test ran on a fast, stable connection, so the timeout-then-retry path — the entire failure — never executed. There was no test on a throttled/slow link, no test of "what happens when a request times out but actually succeeds," and no idempotency check. It's the exact blind spot behind why mobile bugs escape web-first teams: the network is a variable, and we'd treated it as a constant.
The reusable lesson
Test mobile (and any network-dependent) flows on realistic, hostile networks, not just the office Wi-Fi — throttle to slow 3G, introduce latency and packet loss, drop and restore the connection mid-request. For anything that charges, books, or mutates, assume retries will happen and test idempotency: same intent twice should mean one effect. The conditions you don't test are the conditions your users will find for you.
Network-condition case lessons
- Test on throttled / slow / flaky networks, not just fast stable Wi-Fi
- Simulate request timeout-then-late-success — does the app double-act?
- Set client timeouts to real-world latency, not Wi-Fi latency
- For payments/bookings/mutations, test idempotency: same intent twice → one effect
- Drop and restore the connection mid-request; switch Wi-Fi ↔ cellular
- "Can't reproduce at my desk" on a mobile bug is a prompt to change the network, not close the ticket
// RELATED QA.CODES RESOURCES
Common Bug
// related
The bug that only happened after daylight saving time changed
A case study: a scheduling bug that stayed invisible until the clocks changed — and the test scenarios that would have caught it.
The checkout bug that passed every happy-path test
Every checkout test was green, but combining two discounts and a gift card drove the total negative — and issued credit. A case study in testing invariants, not just features.