Catching Race Conditions by Hand: Double Submit and Stale Reads

Written By  Crosscheck Team

Content Team

July 20, 2026 8 minutes

Catching Race Conditions by Hand: Double Submit and Stale Reads

Catching race conditions by hand: double submit and stale reads

A customer calls to say they were charged twice for invoice INV-1042. Two payments of $250.00, four seconds apart, from one click. The developer cannot reproduce it, because on a fast laptop the button disables before a second click can land. On the customer's connection, it did not.

That is a race condition. Race condition testing sounds like it needs special tooling, but you can do it by hand today, with the browser you already have.

Short version

  • A race condition is when two things happen at the same time and the result depends on which one finishes first.
  • Two browser tabs on the same record will expose stale reads and lost updates.
  • Network throttling in Chrome DevTools widens the gap where the bug lives.
  • Fast double clicks on any submit button find duplicate records and double charges.
  • Write the report as an exact order of actions, with timing and what each tab showed.

What a race condition is

Two things happen at the same time, and the result depends on which one finishes first.

That is the whole definition. Your click and a background save. Two tabs saving the same record. Two search requests coming back in the wrong order. The code works when things happen in the expected order, and breaks when they do not.

Developers rarely see these bugs. Their machines are fast, their network is local, and their test data is small. The gaps where race conditions live are measured in milliseconds on their laptop and in seconds on a real user's phone. Your job is to widen those gaps on purpose.


Technique 1: the two-tab test

The simplest concurrency test in existence. Concurrency just means more than one thing happening at once. Two browser tabs give you two users for free.

Recipe: the stale read and the lost update

A stale read is when the screen shows old data that has already changed elsewhere. A lost update is when one person's save quietly overwrites another person's save.

  1. Log in as [email protected] at https://staging.example.com.
  2. Open invoice INV-1042 in tab A.
  3. Open the same invoice INV-1042 in tab B. Do not reload tab A.
  4. In tab A, change the amount to $250.00 and save. Confirm it saved.
  5. Switch to tab B without reloading. Note what the amount field still shows. This is your stale read.
  6. In tab B, change the customer note to "Approved by finance" and save.
  7. Reload both tabs and check the amount.

If the amount is back to the old value, you found a lost update. Tab B sent the whole record, including the amount it loaded before your first save, and the server accepted it. The work you did in step 4 is gone with no warning.

Good software stops you here. It shows a message like "This invoice was changed by someone else. Reload before saving." That is optimistic locking, which means the server checks a version number before it accepts your save.

Recipe: logout and session

  1. Open the dashboard in tab A and the invoice list in tab B.
  2. Log out in tab A.
  3. Go to tab B and click any button that saves data.

Tab B should send you to the login screen. Watch for the bad outcomes: a silent failure, a raw error, or a save that appears to succeed but did nothing.

Recipe: role changes

  1. In tab A, log in as an admin and open the members page.
  2. In tab B, log in as [email protected] and open a page only editors can use.
  3. In tab A, change the role of [email protected] from Editor to Viewer.
  4. In tab B, without reloading, submit the form.

Permission checks that live only in the browser will let the submit through. That is a real security bug, not a cosmetic one.


Technique 2: throttle the network

Throttling means slowing the network on purpose so requests take longer. It turns a 150 ms gap into a 3 second gap, and you can click inside it.

Recipe: set up throttling in Chrome

  1. Press F12 on Windows or Linux, or Cmd+Option+I on Mac, to open DevTools.
  2. Open the Network tab.
  3. Find the throttling dropdown, usually labelled No throttling.
  4. Pick Slow 4G to start.
  5. For harder tests, choose Add custom profile and set download to 200 kb/s, upload to 200 kb/s, and latency to 2000 ms. Latency is the delay before any data arrives.
  6. Tick Preserve log so the request list survives page changes.

Now reload the page and start clicking during the wait.

Three things to test in the gap

  1. Loading states. Submit a form and watch. Is there a spinner, a disabled button, or nothing at all? Nothing at all is a bug report on its own, because the user will click again.
  2. Double navigation. Click a link, then immediately click a different link while the first page is still loading. Check which page you land on and whether the address bar matches the content.
  3. Out-of-order responses. This one is a classic. In a search box, type car and wait half a second. Then type pet so the box reads carpet. Two requests are now in flight. If the slow response for car arrives after the response for carpet, the list shows sofa fabric results under the word carpet. Watch the Network tab to confirm the order the responses came back.

The fix for that last one is that the code should ignore any response that is not the newest. When it does not, you get results that do not match what is on screen.


Technique 3: rapid double clicks

The cheapest test in this article. Click submit twice, fast.

Recipe: double submit

  1. Turn on throttling as above.
  2. Open DevTools and keep the Network tab visible.
  3. Go to the invite form and enter [email protected].
  4. Click Send invite twice, as fast as you can.
  5. Count the requests in the Network tab. You want one. Two is the bug.
  6. Reload the members list and count the invites.

Two outcomes are common. Either you get two records, so [email protected] receives two emails and appears twice in the list. Or the server rejects the second one and the browser shows a raw database message such as duplicate key value violates unique constraint "invites_email_key". The second outcome is safer than the first, but showing that text to a user is still a bug.

Do the same on the payment screen for INV-1042. Two charges of $250.00 is a serious defect, and it is the reason teams use idempotency keys, which are unique IDs sent with a request so the server can recognise and ignore a repeat.

Recipe: the disabled button test

  1. Throttle the network to the custom 200 kb/s profile.
  2. Click submit once.
  3. Immediately try to click it again.

The button must be disabled from the first click until the response arrives. If it disables only after the response, it is not protecting anything.

Recipe: refresh during submit

  1. Click submit.
  2. Press F5 while the request is still in flight.
  3. Check whether the record was created once, twice, or not at all.

Is it a race condition or a normal bug

Use this to sort what you find.

SymptomLikely raceRecipe to reproduce
Two identical records, seconds apartDouble submitThrottle, then click submit twice
Your saved change disappearsLost updateTwo tabs, edit and save in both
Screen shows old values after a save elsewhereStale readTwo tabs, save in A, look at B
Search results do not match the search boxOut-of-order responsesType car, wait, type pet
Action works after a role or plan change that should block itStale permissionTwo tabs, change role in A, submit in B
Wrong page content after fast clickingDouble navigationThrottle, click two links quickly

The signal is timing. A normal bug fails the same way every time. A race condition fails sometimes, fails more on a slow connection, and fails more when you go fast. If a bug stops appearing when you slow down and click carefully, treat it as a race until proven otherwise.


How to write it up

A developer needs to recreate your exact timing. Give them these five things.

  1. The exact order of actions, numbered, with the tab named at each step: "Tab A: save. Tab B: save without reloading."
  2. The timing conditions. Name the throttling profile: "Chrome DevTools, custom profile, 200 kb/s, 2000 ms latency."
  3. What each tab showed. Screenshot both, and say which one was in front.
  4. The network evidence. How many requests were sent, to which path, and what status each returned.
  5. How often it happened. "Four times out of ten" is far more useful than "sometimes".

Getting that evidence by hand is fiddly, because you are clicking fast and watching two windows at once. Tools such as Crosscheck capture the network requests and console output from the page as you report the bug, so the duplicate POST /api/invites is in the ticket without you rebuilding it from memory.


Frequently asked questions

Do I need code or a load testing tool for this? No. Two browser tabs and the DevTools throttling dropdown cover most race conditions in a normal web app. Load tools help with server-side races under heavy traffic, which is a different job.

The bug only happens once every ten tries. Should I still report it? Yes. Report it with the frequency written down, such as "1 in 10 attempts". Rare concurrency bugs become common once thousands of users are on slow connections.

Is a duplicate record always a race condition? No. Check whether two requests were actually sent. If the Network tab shows one request and you still see two records, the problem is on the server, not in the click.

Which throttling profile should I use? Start with Slow 4G, because it matches real mobile users. Move to the custom 200 kb/s and 2000 ms latency profile when you need a wider gap to click inside.

Who should own this testing? Anyone who tests forms. Add a double click and a two-tab check to your standard pass on any screen that creates, pays, invites, or edits a shared record.

Related Articles

Contact us
to find out how this model can streamline your business!

Trusted by thousands ofengineering teams worldwide.

Add to Chrome
200+ reviews · 100k+ users
Crosscheck browser extension capture controls

Join the Crosscheck Community

Stay in the loop with Crosscheck's newest features and insights.