Key takeaways
- Concurrency testing checks that your software stays correct when operations overlap in time. It looks for race conditions, deadlocks, lost updates and double processing. It is a bug-finding technique, not a capacity measurement.
- Load testing asks how fast and how stable a system is under many users. Concurrency testing asks whether the answers are still right. A system can pass one and fail the other.
- The bugs need shared state: a balance, a stock count, a single-use coupon, a cache entry. A stateless read endpoint rarely has anything to race on.
- Failures are intermittent, so run the same test many times and with real parallelism. A single green run proves very little.
Concurrency testing is the practice of running operations at the same time to find defects that only appear when they overlap: two requests that both pass a check before either one updates the state, two threads that wait on each other forever, a counter that loses increments. The phrase also gets used for something different, which is loading a system with many simultaneous users. This page is mainly about the first meaning, the bug-finding one, and it points to the right guides for the second.
Two meanings of “concurrency testing”
| Concurrency testing (correctness) | Load testing with concurrent users | |
|---|---|---|
| Question | Does it still give correct results when operations overlap? | How fast and stable is it with N users at once? |
| Typical defects | Race condition, deadlock, lost update, double charge | Slow responses, errors under load, saturated CPU or pool |
| Load needed | Often small (2 to 50 parallel callers), but timed to collide | As much as the scenario calls for |
| Pass condition | Invariants hold (balance never negative, coupon used once) | Response time and error rate stay within targets |
| Where to read more | This page | Calculate concurrent users, virtual users |
Search data for this URL was too thin to tell which sense visitors expect: Search Console shows no impressions for the page in the last 90 days. The searches that do reach loadfocus.com for “concurrency testing” land on load testing templates, so both senses matter, and the rest of this guide keeps them apart.
What concurrency bugs look like
Race condition. The result depends on the order in which threads or requests happen to run. The classic form is check-then-act: read a value, decide, then write, with a gap in between that another caller can use.
Lost update. Two callers read the same value, both add to it, and the second write overwrites the first.
Deadlock. Two callers each hold a lock the other needs and both wait forever. Databases usually detect and abort one transaction; application locks usually do not.
Double processing. The same event is handled twice, for example a payment retry that charges twice, or a message consumed by two workers.
None of these show up in a test that sends one request at a time, and most do not show up reliably in a test that merely sends many requests in a stream.
A race condition you can run
This Python program models a bank account with 100 in it and starts 10 threads that each try to withdraw 100. Only one withdrawal can succeed. The unsafe version checks the balance, then updates it, with a short pause between the two steps to stand in for any real delay such as a database call:
import threading, time
class Account:
def __init__(self, balance):
self.balance = balance
self.lock = threading.Lock()
def withdraw_unsafe(self, amount):
if self.balance >= amount: # check
time.sleep(0.001) # the gap another thread can use
self.balance -= amount # act
return True
return False
def withdraw_safe(self, amount):
with self.lock: # check and act as one step
if self.balance >= amount:
self.balance -= amount
return True
return False
I ran it with Python 3.10.11, three times for each version. Output:
withdraw_unsafe: 10 of 10 withdrawals of 100 succeeded, final balance -900
withdraw_unsafe: 10 of 10 withdrawals of 100 succeeded, final balance -900
withdraw_unsafe: 10 of 10 withdrawals of 100 succeeded, final balance -900
withdraw_safe: 1 of 10 withdrawals of 100 succeeded, final balance 0
withdraw_safe: 1 of 10 withdrawals of 100 succeeded, final balance 0
withdraw_safe: 1 of 10 withdrawals of 100 succeeded, final balance 0
All ten threads passed the check before any of them reached the update, so the account ended at -900. The fix is to make check and update one atomic step with a lock. The sleep is there to make the race appear on every run; without it the gap is microseconds wide and the bug shows up rarely, which is exactly what makes real races hard to catch. Python’s own thread switching, the interpreter lock and your hardware will change how often it fires, so treat the numbers as an illustration, not a benchmark.
A deadlock you can run
Two threads take two locks in opposite order:
import threading, time
a, b = threading.Lock(), threading.Lock()
def t1():
with a:
time.sleep(0.1)
if not b.acquire(timeout=1): print("t1: gave up waiting for b (deadlock)")
else: b.release()
def t2():
with b:
time.sleep(0.1)
if not a.acquire(timeout=1): print("t2: gave up waiting for a (deadlock)")
else: a.release()
x = [threading.Thread(target=t1), threading.Thread(target=t2)]
[t.start() for t in x]; [t.join() for t in x]
Output from the same machine:
t2: gave up waiting for a (deadlock)
t1: gave up waiting for b (deadlock)
I added the one-second timeout so the script ends; without it both threads wait forever. The standard fix is to take locks in the same order everywhere (always a before b), or to avoid holding one lock while asking for another. Timeouts on lock acquisition, as shown, turn a silent hang into a logged failure.
The same bug through HTTP
Most teams meet this bug as a web or API defect: a single-use coupon redeemed twice, the last item in stock sold twice, a double-clicked “pay” button. I wrote a small Python server whose /redeem/<code> endpoint checks a set of used codes, waits 10 ms for a pretend database write, then records the code. Twenty simultaneous POSTs for one coupon gave this:
unsafe: 20 simultaneous requests for one single-use coupon -> 7 redeemed, 13 refused
safe : 20 simultaneous requests for one single-use coupon -> 1 redeemed, 19 refused
A second run gave 5 redeemed for the unsafe version and again 1 for the locked one, so the count of duplicates changes from run to run and the correct answer does not. In production the equivalent of the lock is usually a database constraint (a unique index on the redemption), an atomic conditional update, or an idempotency key sent by the client, not an in-process lock, because several servers run the same code.
To aim a tool at this kind of defect, the request has to arrive together. In JMeter the Synchronizing Timer blocks threads until a set number are waiting and then releases them all at once, which is how you fire the 20 coupon requests in the same instant. The manual warns that it blocks only within one JVM, and that with a timeout of 0 and too few threads the test pauses forever, so set a timeout. The ramp-up explanation in the ramp-up guide covers the opposite situation, spreading starts out.
How to find concurrency bugs
- Make the collision likely. Release many callers at the same instant (a barrier, a synchronizing timer, a thread pool started together) rather than ramping up slowly.
- Repeat. Run the test hundreds of times. One pass that finds nothing is not evidence.
- Assert invariants, not outputs. Check “balance is never negative”, “this coupon has one redemption row”, “stock equals starting stock minus orders”. Do not compare response times.
- Use a race detector where one exists. Go’s
-raceflag (go test -race ./...) reports data races with stack traces, but its documentation says it only finds races that happen at run time, so a clean run says nothing about code paths your tests did not execute. Run a race-enabled binary under a realistic workload as well. - Check the database, not only the response. Duplicates often hide behind two “200 OK” answers.
- Read the code for check-then-act. An
iffollowed by a write on shared state is the pattern to search for.
Where load tests fit
A load test with many virtual users can expose concurrency bugs as a side effect: duplicated rows, deadlocked transactions and a rising error rate at high concurrency are all reported by its error counts. That makes load tests useful for this work, but they are not designed for it, because the aim of a load test is realistic traffic spread out over time, and a race needs a collision at one instant on one piece of data. If you want the concurrent-user side, start with how many simultaneous users you have to design for in the concurrent users guide and the virtual users guide, then see the types of performance testing for where stress, spike and soak tests fit. They are load shapes, not parts of concurrency testing.
To run a parameterized JMeter script from several cloud regions, LoadFocus accepts standard .jmx files; see its JMeter load testing page for the current limits. Prepare the data the script sends, such as one coupon code per user, as described in the JMeter parameterization guide.
Frequently Asked Questions
What is concurrency testing in software testing?
It is testing how software behaves when operations overlap in time, with the goal of finding race conditions, deadlocks, lost updates and double processing. Success means the system’s rules still hold, for example that a single-use coupon is redeemed once, however the operations interleave.
What is the difference between concurrency testing and load testing?
Concurrency testing checks correctness under overlapping operations and can use few callers if they collide on the same data. Load testing measures response time, throughput and errors under a chosen number of users. The first finds wrong answers, the second finds slow or failing ones.
What is the difference between concurrent users and concurrency testing?
Concurrent users is a number: how many people are active at the same time. Concurrency testing is an activity that checks the system’s behavior when activity overlaps. You calculate concurrent users to design a load test, and you run concurrency tests to look for defects.
How do you test for race conditions?
Release many callers at the same moment against the same piece of shared state, repeat the run many times, and assert the invariant (one redemption, balance not negative) rather than the response. Use a race detector for languages that have one.
Why is concurrency testing hard to do?
The failures depend on timing, so they are intermittent and may not appear on a developer machine. Repetition, deliberate collisions and invariant checks are what make them reproducible.
Sources
- Go race detector, checked 2026-10-23.
- JMeter component reference, Synchronizing Timer, checked 2026-10-23.
- Code and output above: Python 3.10.11, run locally 2026-10-23.