Race Conditions in Software: How to Test What Happens When Things Happen at the Same Time

Race Conditions in Software: How to Test What Happens When Things Happen at the Same Time



Introduction

Imagine an online shopping application where only one item is left in stock.

Two customers click the Buy Now button at almost exactly the same time.

Both customers receive an order confirmation. The inventory system now shows that the item is sold out, but the company has accepted two orders for a single product.

What went wrong?

The application may have worked perfectly when tested with one user. The problem appeared only when two requests interacted with the same data at the same time.

This is a classic example of a race condition.

Race conditions are among the most challenging defects to identify in software testing. They are unpredictable, timing-dependent, and often difficult to reproduce consistently.

As Quality Engineers, we need to move beyond testing whether an operation works in isolation. We must also verify whether the system remains correct when multiple operations compete for the same resource.

1. What Is a Race Condition?

A race condition occurs when the behavior of a system depends on the timing or ordering of concurrent operations, and that ordering is not properly controlled.

In simple terms:

Two or more operations are competing to access or modify shared data, and the final result depends on which operation executes first or how their execution overlaps.

Race conditions can occur in:

  • Multithreaded applications

  • REST APIs processing concurrent requests

  • Database transactions

  • Distributed systems and microservices

  • Background jobs and message queues

  • Web applications with asynchronous operations

The important point is that a race condition is not necessarily caused by two operations executing at precisely the same instant. It can happen when their execution overlaps in an unexpected way.

A race condition can also become a security vulnerability when an attacker can exploit the timing gap between a validation and a subsequent action. This is commonly known as a Time-of-Check to Time-of-Use (TOCTOU) issue. https://community.owasp.org/pages/vulnerabilities/race_conditions

2. A Simple Example: Inventory Management

Consider an application with the following inventory:

Product: Laptop
Available stock: 1

Two customers attempt to purchase the same laptop.

The expected business rule is:

The system must never accept more orders
than the available inventory.

However, the application might process the requests like this:

Customer A                 Customer B
    |                          |
    | Read stock = 1           |
    |                          | Read stock = 1
    | Check stock > 0          |
    |                          | Check stock > 0
    |                          |
    | Reduce stock by 1        |
    |                          | Reduce stock by 1
    |                          |
    | Order confirmed          | Order confirmed

Both requests read the same initial value before either update becomes visible.

The system has now accepted two orders when only one item was available.

This is not simply a performance issue. It is a violation of a business rule.

The key testing question is:

Can two individually valid requests produce an invalid combined result?

3. Why Are Race Conditions Difficult to Test?

Race conditions are difficult because the same test can produce different results depending on timing.

A test might pass 100 times when requests are executed sequentially, but fail when requests overlap.

Some common challenges include:

  • The defect may occur only within a very small time window.

  • Network latency affects when requests reach the server.

  • The issue may appear only under specific workloads.

  • A test may pass locally but fail in a CI/CD environment.

  • The API response may look correct while the database state is inconsistent.

  • A race may involve multiple services or asynchronous background processing.

A conventional functional test usually validates a single request and its response.

A concurrency test must validate the combined outcome of multiple operations.

This difference is important.

A successful HTTP 200 response does not necessarily mean the system handled a concurrent operation correctly.

4. Where Should QA Engineers Look for Race Conditions?

Not every endpoint needs intensive concurrency testing. The best candidates are operations that read, validate, and modify shared state.

Application areaPotential race condition
InventoryTwo users purchase the last item
PaymentsDuplicate payment processing
BankingTwo withdrawals exceed the balance
CouponsA single-use coupon is redeemed twice
BookingTwo customers reserve the same seat
InsuranceTwo requests create duplicate policies
ClaimsMultiple requests approve the same claim
AuthenticationConcurrent password reset and login operations
Background jobsThe same message is processed more than once

A useful starting point is to review business rules that contain phrases such as:

  • Only once

  • One active record

  • Cannot exceed

  • Must be unique

  • Available until consumed

  • Must happen before

  • Cannot be processed twice

These rules often indicate that the application needs protection against concurrent operations.

5. How to Test Race Conditions

A practical testing strategy can be divided into five steps.

Step 1: Identify the business invariant

Before writing an automation script, define what must always remain true.

For example:

Given:
  Available inventory = 1

When:
  Two customers submit an order concurrently

Then:
  Only one order must be accepted
  Inventory must never become negative

This is the most important step.

Without a clear expected outcome, a concurrency test may simply generate traffic without detecting a real defect.

Step 2: Establish a sequential baseline

First, execute the operation normally.

Verify that:

  • The request succeeds.

  • The correct database state is created.

  • The business rule is respected.

  • The response matches the expected contract.

This confirms that the basic functionality is working.

Step 3: Send concurrent requests

Now submit multiple requests against the same resource.

For example:

Request A ─────┐
               ├──> Inventory API
Request B ─────┘

The goal is to make the requests overlap as closely as possible.

Tools such as Postman, k6, JMeter, and Burp Suite can help generate concurrent requests.

However, a high number of requests alone does not guarantee that the race window has been reached. Synchronization and controlled timing make the test more effective.

Step 4: Validate the final state

Do not stop at checking the HTTP responses.

Validate the persistent state of the application.

For example:

Expected:
  Accepted orders = 1
  Inventory = 0

Unexpected:
  Accepted orders = 2
  Inventory = -1

A test should check both the API behavior and the business outcome.

Step 5: Repeat the test

Race conditions may not happen on every execution.

Repeat the test using different concurrency levels and controlled timing.

For example:

2 concurrent requests
5 concurrent requests
10 concurrent requests
50 concurrent requests

The goal is not simply to increase traffic. It is to discover whether the system preserves its business invariants under concurrent execution.


 

Comments