Concurrency Testing
Introduction
Modern APIs are designed to serve many users at the same time. At any moment, hundreds or thousands of clients may send requests to the same endpoint. Some requests only read data, while others create, update, delete, reserve, transfer, or approve important business records. When two or more requests touch the same resource at the same time, the API must preserve correctness.
Concurrency Testing verifies that an API correctly handles multiple simultaneous requests while maintaining data integrity, transaction safety, and system stability. It is especially important for workflows where users compete for the same resource, such as buying the last product in stock, booking the last seat on a flight, withdrawing money from the same account, redeeming a coupon, updating an employee record, or confirming an appointment slot.
If concurrency is not handled properly, the API may create duplicate records, lose updates, corrupt data, allow negative inventory, produce incorrect balances, or leave the database in an inconsistent state. These failures are often difficult to reproduce because they depend on timing. That is why deliberate Concurrency Testing is necessary.
What Is Concurrency Testing?
Concurrency Testing is a type of non-functional testing that verifies how an API behaves when multiple users, services, threads, or processes access the same resources simultaneously. It focuses on correctness under simultaneous access rather than only speed or throughput.
In simple terms, Concurrency Testing ensures an API correctly handles multiple simultaneous requests without causing data inconsistencies, race conditions, lost updates, deadlocks, duplicate records, or system failures. A successful concurrency test proves that the API protects shared resources when requests overlap in time.
Concurrency Testing is different from sending many requests sequentially. Sequential testing sends one request after another. Concurrency Testing attempts to make requests overlap so the system must handle real simultaneous access. The timing overlap is what exposes concurrency defects.
Why Concurrency Testing Is Important
Concurrency Testing is important because most production systems are multi-user systems. Even if each individual API request works correctly, the system may fail when multiple requests act on the same data at the same time. Functional tests often miss these problems because they test one path at a time.
It helps prevent race conditions. A race condition occurs when the final result depends on the timing of concurrent operations. If two users read the same stock value and both attempt to purchase the final item, the API must ensure that only one purchase succeeds.
Concurrency Testing also prevents lost updates. If two users update the same employee record, one update should not silently overwrite the other. The system should merge changes safely, reject conflicting updates, or use locking/versioning rules depending on the design.
It detects deadlocks and transaction issues. Multiple database transactions may lock different rows or tables and wait for each other indefinitely. A good system should avoid deadlocks where possible and handle them gracefully when they occur.
Concurrency Testing improves reliability because it validates thread safety, transaction integrity, database consistency, and resource protection. These are core qualities for APIs that process money, inventory, bookings, healthcare records, employee data, and other important business information.
Concurrency Testing Workflow
A typical Concurrency Testing workflow starts by identifying a shared resource. This could be an account balance, product stock, booking seat, employee record, coupon code, order state, appointment slot, or document record. The selected resource should be one where simultaneous access can create risk.
Next, the test prepares the initial state. For example, product stock is set to one item, account balance is set to a known amount, or one seat is made available. This controlled starting state makes the expected outcome clear.
The test then sends simultaneous requests. Multiple users or threads perform the same or conflicting operation at nearly the same time. The test collects responses, status codes, response bodies, timing, errors, and logs.
Finally, the test verifies the final system state. This is critical. API responses alone are not enough. The database, audit log, event queue, transaction table, inventory table, or account ledger should be checked to confirm that data remained consistent.
Example Scenario
Suppose an API manages product inventory and the current product stock is one. Two users attempt to purchase the product at the same time. The expected result is that only one purchase succeeds. The second request should fail with an appropriate response such as out of stock, conflict, or business-rule violation.
If both purchase requests succeed, the system has oversold inventory. If stock becomes negative, the database state is incorrect. If one request succeeds and the other fails cleanly, the system likely handled concurrency correctly for that scenario.
This same pattern appears in many domains. Only one user should book the last seat. Only one withdrawal should succeed if the account cannot cover both withdrawals. Only one coupon redemption should be accepted if the coupon is single-use.
What Happens Without Proper Concurrency Control?
Without proper concurrency control, APIs can produce duplicate orders, lost updates, negative inventory, incorrect account balances, corrupted records, inconsistent workflow states, duplicate bookings, double payments, or missing audit trails.
These issues are often more serious than simple response errors because they affect business data. A slow response can be retried. Corrupted financial or inventory data may require manual investigation, customer support, refunds, reconciliation, or production fixes.
Concurrency failures can also damage trust. If a customer is charged twice or books a seat that is later unavailable, the issue is visible and frustrating. Concurrency Testing helps prevent these failures before production.
Common Concurrency Problems
Common concurrency problems include race conditions, lost updates, dirty reads, non-repeatable reads, phantom reads, deadlocks, thread safety issues, duplicate processing, stale reads, and inconsistent event ordering. Each problem affects data consistency in a different way.
Race Condition
A race condition occurs when two or more requests access and modify shared data simultaneously, producing unpredictable or incorrect results. The final result depends on which request finishes first.
For example, an account balance is 100 dollars. User A withdraws 100 dollars while User B also withdraws 100 dollars. If both requests read the original balance before either update is committed, both may think the withdrawal is allowed. The final balance may become negative, or both transactions may be incorrectly accepted.
The expected behavior is that only one withdrawal succeeds when the balance cannot support both. The API should enforce this through transaction rules, locking, atomic updates, or database constraints.
Lost Update
A lost update occurs when two users update the same record and one update silently overwrites the other. For example, User A updates an employee salary while User B updates the employee department. If both submit changes based on the same original record and the second update overwrites the first, information is lost.
Lost updates can be prevented with optimistic locking, version numbers, timestamps, conditional updates, merge logic, or conflict responses. Concurrency Testing should verify that the API follows the intended conflict-handling design.
Dirty Read
A dirty read occurs when one transaction reads data that another transaction has modified but not yet committed. If the first transaction later rolls back, the second transaction has read data that never became valid.
Dirty reads can cause business logic to act on invalid information. Database isolation levels usually control whether dirty reads are allowed. API testers should understand the expected isolation behavior for critical workflows.
Non-Repeatable Read and Phantom Read
A non-repeatable read occurs when the same query returns different values within one transaction because another transaction updated the data. A phantom read occurs when a repeated query returns additional or missing rows because another transaction inserted or deleted matching records.
These problems matter in APIs that calculate totals, validate limits, reserve resources, or make decisions based on sets of rows. For example, a booking API may query available seats twice and get different answers because another transaction inserted a reservation.
Deadlocks
A deadlock occurs when two transactions wait for each other indefinitely. Transaction A may lock the employee table and wait for the salary table, while Transaction B locks the salary table and waits for the employee table. Neither transaction can continue until the database detects and resolves the deadlock.
APIs should handle deadlocks gracefully. The system may retry safe operations, return a controlled error, or rollback cleanly. Concurrency Testing can reveal whether deadlocks occur under simultaneous access and whether recovery is acceptable.
Thread Safety
A thread-safe API ensures that multiple concurrent requests do not corrupt shared resources in application memory. Thread safety is achieved through synchronization, locks, atomic operations, immutable data, stateless design, thread-safe collections, optimistic locking, pessimistic locking, and careful transaction boundaries.
Modern APIs should avoid storing request-specific state in shared mutable objects. If shared state is necessary, it must be protected. Thread safety issues can appear only under concurrent execution, which makes them difficult to find with ordinary functional testing.
Database Locking
Database locking is one of the main ways systems protect data during concurrent operations. The two common approaches are optimistic locking and pessimistic locking. The correct choice depends on business rules, conflict frequency, performance needs, and user experience.
Optimistic Locking
Optimistic Locking assumes conflicts are rare. It allows users to read and prepare changes without locking the record immediately. When the update is submitted, the system checks a version number, timestamp, or similar marker. If another transaction updated the record first, the current update fails.
This approach is useful when conflicts are uncommon and the system wants to avoid long-held locks. API responses may use status codes such as conflict or precondition failure depending on the design. Testers should verify that stale updates are rejected and that the client receives a clear message.
Pessimistic Locking
Pessimistic Locking locks the record before updating it. Other transactions must wait until the lock is released. This approach is useful when conflicts are likely or when the business operation must be strictly serialized.
The drawback is reduced concurrency and possible waiting. Poor lock design can create deadlocks or slow performance. Concurrency Testing should verify both correctness and acceptable behavior under lock contention.
Concurrency Testing Process
The process begins by identifying high-risk operations. These are usually operations that modify shared data: payments, withdrawals, purchases, bookings, coupon redemptions, status transitions, inventory updates, approvals, deletes, and profile updates.
Next, create a controlled starting state. The account balance, product stock, seat availability, record version, or coupon usage count should be known before the test starts. Without a controlled state, final verification becomes ambiguous.
Then execute simultaneous requests using tools or code that can truly overlap operations. The requests should target the same resource or related resources. The test should collect all responses and timing details.
After execution, verify API responses, database state, audit logs, event messages, and downstream effects. The final state matters more than individual response timing. A test passes only if the business invariant remains true.
Concurrency Testing in API Testing
QA engineers should verify simultaneous updates, simultaneous deletes, simultaneous inserts, duplicate prevention, data consistency, transaction integrity, deadlocks, thread safety, locking behavior, rollback behavior, audit logs, and error responses.
Testing should include both same-operation and conflicting-operation scenarios. Same-operation examples include two users buying the same product. Conflicting-operation examples include one user deleting a record while another updates it.
Read and write combinations should also be tested. A read during a write should return data according to the expected consistency model. Some systems allow eventual consistency, while financial systems usually require stronger consistency.
Example Test Scenarios
In banking, an account has a balance of 1,000 dollars. Two withdrawal requests of 800 dollars are submitted at the same time. The expected result is that only one withdrawal succeeds. The final balance should be 200 dollars, not negative 600 dollars.
In e-commerce, stock is one item and two purchase requests are submitted at the same time. One order should succeed and one should receive an out-of-stock response. Inventory should not become negative.
In employee management, two users update the same employee simultaneously. The expected result depends on business rules. The API may merge non-conflicting fields, reject stale updates, or require the second user to reload the latest version. It should not silently lose data.
In ticket booking, one seat is available and multiple users attempt to book it. Only one booking should succeed. The seat should not be assigned to multiple passengers.
Validation Checklist
Before testing, define the shared resource, initial state, expected invariant, number of concurrent users, request payloads, timing approach, database checks, and cleanup strategy. The expected result must be explicit.
During testing, verify data consistency, transaction integrity, duplicate prevention, lost update prevention, correct locking, thread safety, deadlock handling, error responses, database state, audit logs, and downstream events.
After testing, inspect final records, counts, balances, versions, timestamps, audit history, event messages, and logs. Also verify that failed requests fail for the correct reason and do not leave partial state behind.
REST Assured with Java Concurrency
REST Assured itself is mainly a functional API testing library. It does not generate concurrent users as efficiently as dedicated load tools, but it can be combined with Java concurrency utilities for focused concurrency scenarios.
ExecutorService executor = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
executor.submit(() ->
given()
.when()
.post("/orders")
);
}
This approach is useful for small targeted tests, but the framework must collect responses, wait for all tasks to finish, and verify the final database state. For larger workloads, dedicated tools are usually better.
JMeter
JMeter supports concurrency through Thread Groups. A test can create 100 threads that submit POST /orders for the same product. JMeter can show response counts, success counts, failure counts, response times, and errors.
For Concurrency Testing, JMeter assertions should be paired with database validation. It is not enough to see that one request failed. The final stock, order count, payment state, and audit log must be correct.
k6 Example
k6 can generate concurrent virtual users using a simple script:
import http from 'k6/http';
export const options = {
vus: 100,
duration: '30s',
};
export default function () {
http.post('https://api.example.com/orders');
}
Real concurrency tests should parameterize users carefully and target controlled resources. If every virtual user buys a different product, the test may be a load test, not a concurrency conflict test.
Karate
Karate supports parallel execution and can run feature files concurrently. For example:
Runner.path("classpath:features")
.parallel(10);
Karate is useful when API tests already exist as feature files and the team wants to execute scenarios in parallel. As with other tools, final state validation is essential.
Real-World Examples
In banking, Concurrency Testing verifies simultaneous withdrawals, fund transfers, loan payments, bill payments, and account updates. The system must protect balances and transaction history.
In healthcare, it verifies appointment booking, patient record updates, prescription changes, insurance updates, and lab result workflows. Data correctness and audit trails are critical.
In e-commerce, it verifies checkout, inventory updates, coupon redemption, payment confirmation, cart updates, and order status changes. Overselling and duplicate charges must be prevented.
In airline reservation systems, it verifies seat booking, flight cancellation, check-in operations, upgrades, and seat changes. One seat should not be assigned to more than one passenger.
Concurrency Testing vs Load Testing
Concurrency Testing focuses on simultaneous access to shared resources and validates data consistency. Load Testing focuses on overall system performance under expected traffic and validates response time, throughput, and capacity.
A Load Test may send thousands of requests to different resources and measure performance. A Concurrency Test may send twenty requests to the same product, account, or seat and verify that the final state is correct. Both are useful, but they find different classes of defects.
Designing Reliable Concurrency Tests
Concurrency tests can be flaky if the timing is not controlled. A test that sends requests in a loop may still execute them sequentially enough that the conflict never occurs. To improve reliability, tests can use thread barriers, synchronized starts, short test windows, controlled fixtures, and dedicated test resources.
Each test should create or reserve its own data. Sharing the same resource across many automated tests can produce false failures. If a product, seat, or account is used by another test at the same time, the final state becomes difficult to interpret.
Tests should also be repeatable. After each run, cleanup or reset logic should restore the system to a known state. If cleanup is not possible, the test should create unique resources for each execution and verify them independently.
Idempotency and Duplicate Prevention
Idempotency is closely related to concurrency because real systems often receive repeated or overlapping requests. A client may retry after a timeout, a user may double-click a submit button, or a gateway may resend a request after a temporary network issue. If the API treats every repeated request as a new operation, duplicate orders, duplicate payments, or duplicate bookings can occur.
Idempotency keys help solve this problem. The client sends a unique key with the request, and the server uses that key to recognize repeated attempts for the same operation. If the first request already succeeded, the repeated request can return the original result instead of creating another transaction. This is especially important for payment, order, booking, and transfer APIs.
Concurrency Testing should include duplicate prevention scenarios. For example, send the same order request multiple times concurrently with the same idempotency key. The expected result is one created order and consistent responses for duplicate attempts. Then send similar requests with different keys to verify that the API does not incorrectly block legitimate separate operations.
Database unique constraints can also protect against duplicates. For example, a coupon redemption table may enforce a unique combination of coupon ID and user ID. Even if two concurrent requests reach the database, only one insert should succeed. The API should convert the duplicate constraint failure into a clear business response rather than an uncontrolled server error.
Transaction Isolation and API Behavior
Transaction isolation defines how database transactions interact with each other while running at the same time. The chosen isolation level affects dirty reads, non-repeatable reads, phantom reads, locking, and performance. API testers do not need to tune the database, but they should understand that isolation level can change concurrency behavior.
Lower isolation levels may improve performance but allow more visibility into concurrent changes. Higher isolation levels may improve consistency but increase locking, waiting, and deadlock risk. The correct choice depends on business requirements. A banking transfer usually needs stronger consistency than a product recommendation read.
Concurrency tests should be aligned with expected isolation behavior. If the API claims strong consistency, tests should verify that reads and writes do not expose invalid intermediate states. If the API uses eventual consistency, tests should verify that temporary differences converge to the correct final state within an acceptable time.
Isolation behavior should also be documented in test expectations. Otherwise, testers may report a defect for behavior that is actually part of the design, or miss a defect because the expected consistency model was unclear.
Debugging Intermittent Concurrency Failures
Concurrency failures are often intermittent because they depend on timing. A test may pass nine times and fail once. This does not mean the failure should be ignored. Intermittent concurrency failures often indicate real production risk because production traffic creates timing combinations that are hard to predict.
Good debugging starts with evidence. Capture request IDs, timestamps, thread names, user IDs, resource IDs, transaction IDs, database statements, lock wait events, response bodies, and final database state. Correlating these details makes it easier to reconstruct what happened.
Logs should include enough context to connect each request with the affected resource. For example, if two requests update the same employee, both logs should include the employee ID, version number, request ID, and transaction outcome. Without correlation, concurrency problems can be extremely difficult to analyze.
When a concurrency failure is found, rerun the test with the same initial state and increased logging. If possible, reduce the scenario to the smallest reproducible case. A simple two-request test is easier to debug than a 100-user test. Once the defect is understood, larger tests can verify the fix under realistic conditions.
Best Practices
Use production-like datasets and realistic workflows. Execute truly simultaneous requests, not sequential requests. Verify the database after each test. Test both read and write operations. Validate transaction rollback and deadlock handling.
Monitor database locks, thread pools, connection pools, logs, and transaction failures during the test. Concurrency problems often appear in infrastructure and database logs before they appear clearly in API responses.
Test with realistic user volumes for the business scenario. For inventory or booking conflicts, a small number of simultaneous users may be enough. For collaboration systems or trading systems, higher concurrency may be needed.
Automate high-risk concurrency scenarios and run them regularly. Concurrency bugs can be reintroduced when transaction logic, database queries, locking strategy, or caching behavior changes.
Common Mistakes
One common mistake is testing sequential requests and calling it concurrency testing. True concurrency requires overlapping execution. Another mistake is checking only API responses and ignoring the database state.
Ignoring transaction isolation is also risky. Different database isolation levels can change read and write behavior. Testers should understand whether the API expects read committed, repeatable read, serializable, or another isolation model.
Not testing shared resources misses the point of Concurrency Testing. If every request uses a different account or product, the test may not expose race conditions or lost updates.
Ignoring locking mechanisms is another issue. If the system uses optimistic locking, stale update tests should be included. If the system uses pessimistic locking, waiting and deadlock behavior should be verified.
Advantages
Concurrency Testing detects race conditions, prevents data corruption, improves reliability, validates thread safety, protects transaction integrity, and ensures consistent data. It helps teams find defects that ordinary functional tests often miss.
It also increases confidence in critical workflows. Payment, banking, booking, inventory, healthcare, and employee data workflows must remain correct even when multiple users act at the same time.
Concurrency Testing supports better architecture decisions. It can reveal whether optimistic locking, pessimistic locking, unique constraints, transactions, queues, or idempotency keys are needed for a workflow.
Limitations
Concurrency Testing can be difficult because timing-sensitive issues are hard to reproduce. Some bugs appear only under specific timing, data, or infrastructure conditions. Debugging intermittent failures can take time.
It requires realistic concurrent workloads and careful test design. A poorly designed test may miss the issue or create false failures. Some scenarios may need specialized tools, database access, logging, or controlled test environments.
Concurrency Testing also does not replace Load Testing. It validates correctness under simultaneous access, but performance under broad traffic still needs separate testing.
Interview Questions
A common interview question is: what is Concurrency Testing? A strong answer is that Concurrency Testing verifies that an API correctly handles multiple simultaneous requests while maintaining data consistency and system stability.
Another question is: why is Concurrency Testing important? It prevents race conditions, lost updates, duplicate records, deadlocks, and data corruption.
If asked about common concurrency issues, mention race conditions, lost updates, dirty reads, non-repeatable reads, phantom reads, deadlocks, and thread safety issues.
If asked about Optimistic and Pessimistic Locking, explain that Optimistic Locking assumes conflicts are rare and detects them using version numbers or timestamps, while Pessimistic Locking locks data before modification and forces other transactions to wait.
If asked which tools are commonly used, mention Apache JMeter, k6, Gatling, Locust, REST Assured with Java concurrency utilities, Karate, and LoadRunner.
Interview-Ready Explanation
Concurrency Testing is a non-functional testing technique used to verify that an API correctly handles multiple simultaneous requests to the same resource without causing data corruption, race conditions, lost updates, duplicate records, or inconsistent database states. It focuses on validating transaction integrity, thread safety, locking mechanisms, and database consistency when several users perform operations such as creating, updating, deleting, or reading the same data concurrently.
During Concurrency Testing, QA engineers monitor API responses, final database state, transaction behavior, locks, logs, audit entries, and downstream effects while executing parallel requests. Common scenarios include simultaneous bank withdrawals, ticket bookings, product purchases, coupon redemptions, and employee record updates.
Tools such as Apache JMeter, k6, Gatling, Locust, Karate, LoadRunner, and REST Assured combined with Java concurrency utilities are commonly used to simulate concurrent API requests and validate correct behavior under simultaneous access.
Key Takeaway
Concurrency Testing proves whether an API protects shared data when multiple users act at the same time. It is not mainly about speed; it is about correctness under overlap. Race conditions, lost updates, deadlocks, and duplicate records can cause serious business damage if they reach production.
For practical API testing, identify shared resources, create controlled starting states, execute truly simultaneous requests, verify responses, inspect final database state, review logs, and repeat high-risk scenarios regularly. A reliable API must remain correct even when users compete for the same data.