Skip to main content

How to Test a Race Condition in a Web API from the Terminal

· 6 min read
Jagdish Kumawat
Founder @ Dewiride

A race condition doesn't show up when you send one request at a time. You need several at the same moment, and you don't need a load-testing tool: curl with xargs does it on macOS and Linux, and ForEach-Object -Parallel does it in PowerShell. Then check what's in the database, because the status codes alone can look fine.

Prerequisites​

  • An API running on your machine. Mine is on http://localhost:5190.
  • On macOS or Linux: curl and xargs, which are already installed.
  • On Windows: PowerShell 7. -Parallel doesn't exist in Windows PowerShell 5.1. I tested these commands in PowerShell 7.6.6 on macOS, not on Windows.

Step 1: Send Five Requests at Once​

Terminal
seq 5 | xargs -P 5 -I{} curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:5190/api/v1/orders -H "Content-Type: application/json" -d '{"customerName":"Buyer {}","customerEmail":"buyer{}@example.com","lines":[{"productId":13,"quantity":1}]}'
  • xargs -P 5 runs up to 5 at the same time. -I{} puts each number from seq wherever {} appears.
  • -w "%{http_code}\n" prints just the status, and -o /dev/null throws away the body.
  • -ThrottleLimit 5 is PowerShell's equivalent, and -SkipHttpErrorCheck stops a 409 from being treated as a failure.

Step 2: Read the Result​

With the product at one unit in stock:

Output
201
409
409
409
409

Five requests giving one 201 and four 409.

The same in PowerShell:

Output
201
409
409
409
409

Step 3: Check the Database​

Status codes can look right when the data isn't. Compare the numbers that should agree:

mysql
SELECT p.QuantityOnHand,
(SELECT COALESCE(SUM(m.QuantityChange),0) FROM stock_movements m WHERE m.ProductId=p.Id) AS SumOfMovements,
(SELECT COUNT(*) FROM order_lines l WHERE l.ProductId=p.Id) AS OrdersPlaced
FROM products p WHERE p.Id=13;
Output
+----------------+----------------+--------------+
| QuantityOnHand | SumOfMovements | OrdersPlaced |
+----------------+----------------+--------------+
| 0 | 0 | 1 |
+----------------+----------------+--------------+

And here's what a real bug looks like, where five buyers all got a 201:

Output
201
201
201
201
201
Output
+----------------+----------------+--------------+
| QuantityOnHand | SumOfMovements | OrdersPlaced |
+----------------+----------------+--------------+
| 0 | -4 | 6 |
+----------------+----------------+--------------+

Step 4: Run It More Than Once​

A race depends on timing. With the buggy code above, the first round I ran, just after the API started, gave one 201 and four 409, which looked correct:

Output (the same bug, first round)
201
409
409
409
409

So run several rounds, and warm the API up first with an ordinary request.

Step 5: Count Many Requests​

For larger runs, count each status with sort | uniq -c, or Group-Object. These 30 orders use two products in opposite orders, half each way, which is how to look for deadlocks:

Terminal
(seq 15 | xargs -P 15 -I{} curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:5190/api/v1/orders -H "Content-Type: application/json" -d '{"customerName":"Buyer {}","customerEmail":"buyer{}@example.com","lines":[{"productId":1,"quantity":1},{"productId":2,"quantity":1}]}' &
seq 15 | xargs -P 15 -I{} curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:5190/api/v1/orders -H "Content-Type: application/json" -d '{"customerName":"Buyer {}","customerEmail":"buyer{}@example.com","lines":[{"productId":2,"quantity":1},{"productId":1,"quantity":1}]}' &
wait) | sort | uniq -c

Before I sorted the lines in the code:

Output
15 201
15 500

After:

Output
30 201

Common Mistakes​

MistakeWhat happensDo this instead
Using & in a loop in a slow shellRequests can arrive one after anotherUse xargs -P
Reading only the status codesA bug can answer 201 five timesCheck the data
Running it onceA race can pass by luckRun several rounds
Testing against a cold APITiming is differentSend one request first

Troubleshooting​

SymptomLikely causeFix
-Parallel isn't recognisedWindows PowerShell 5.1Install PowerShell 7
Everything answers 500The API has a problem of its ownRead its log
All succeed, and you expected conflictsRequests didn't overlapAdd more, and repeat
zsh: no matches foundA ? in an unquoted addressQuote it
xargs: command line cannot be assembled, too longA long argument on macOSThe fix

Conclusion​

xargs -P and ForEach-Object -Parallel will send requests at the same moment with no extra tools. Run several rounds, and judge the result by the database, not only by the status codes.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.