Concurrency Testing for Web Apps: Double Clicks, Two Tabs, and Retries
A workspace has one seat left. Two invited users accept at nearly the same time. Both see success, and the workspace now exceeds its limit.
Concurrency testing investigates operations whose execution overlaps against shared state. It is different from simply repeating an action: the defect may depend on both operations reading the same old value before either writes a new one.
This is part 8 of Testing Methods in Practice. The examples use controlled test environments and assumed product rules, not production experiments or reported HiTest results.
State the invariant before choosing the trigger
For the seat example, the invariant is that active membership never exceeds capacity. With nine active members and a limit of ten, two valid invitations compete for one seat. Exactly one acceptance may succeed; the other must receive the documented capacity response.
For a document edit, the rule could be optimistic version checking: the first save succeeds, and a stale second save is rejected with a conflict and preserved user input. Another product may intentionally merge edits. Write down the chosen rule before calling a lost update a defect.
Separate three experiments
| Experiment | What overlaps? | What it can reveal |
|---|---|---|
| Sequential repetition | Nothing; first response arrives before second action | Duplicate handling and repeat behavior |
| Same logical operation retried | Requests may overlap, carrying the same operation identity | Idempotency and uncertain-outcome recovery |
| Independent concurrent operations | Different users or operation identities contend for shared state | Capacity races, lost updates, and ordering defects |
A disabled button may prevent a double click from one browser. It cannot serialize two users or stop a retried request that already reached the server.
Stripe's idempotent request documentation illustrates recognizing retries by a request key. Your application's policy must also define key scope, parameter matching, retention, and how genuinely new operations receive new identities. A provider's guarantee does not automatically cover your own database writes or notifications.
Coordinate the seat race instead of hoping for it
Prepare nine members and two pending invitations. Use separate sessions for the invitees. In a test harness, pause both acceptance operations immediately before the critical capacity decision, then release them together.
The barrier must be part of a controlled test seam. Do not ship it as a public feature or assume that clicking two tabs quickly establishes the ordering. If the implementation already locks before reading capacity, instrument or observe the contention rather than bypassing the lock to force an impossible execution.
Record both responses, final member identities, capacity, and any emitted welcome events. The final count should be ten, with only the winning invitee gaining access. Verify the losing invitation's documented state and recovery after a seat becomes free.
Check stale edits with two versions
Open the same document in two sessions at version 3. In the first, change the title and save version 4. In the second, submit an edit based on version 3.
Under optimistic concurrency, the second save must identify the conflict and preserve its draft. Reloading should show the committed version 4. If a merge policy is intended, test that policy with overlapping and non-overlapping fields separately.
Do not use identical edited values for both sessions. Distinct values make a silently overwritten update visible. Keep object and version identifiers in the evidence so you can distinguish an ordering defect from editing two different records.
Include side effects in the outcome
Two responses can refer to one correct final record while sending duplicate emails or charging twice. Count business effects by logical operation. Use sandbox providers and inspect relevant event or ledger records.
For failures after commit, preserve the same operation identity during retry. For a new deliberate action, use a new one. The error recovery guide covers why a failed response can still accompany a committed write.
Choose the right layer for evidence
Use integration or service tests to coordinate precise overlap and verify invariants. Browser checks establish that users see understandable conflict, capacity, or retry outcomes and can continue safely. Browser-only evidence cannot prove every transaction interleaving.
Your exercise: choose one shared counter, one editable record, and one operation with an external side effect. Write an invariant and a coordinated experiment for each. Run the targeted interleavings repeatedly while preserving failures; do not treat a handful of successful races as proof that all possible schedules are safe.
Next in the series: Useful test assertions.
