
Race Conditions in Web Applications: Why Correct Code Produces Wrong Results
Got a project?
Let's discuss your project
Introduction
Ever wondered why an application sometimes produces the wrong result even though the code looks perfectly correct? Why two users performing the same action at nearly the same time can cause duplicate records, incorrect balances, or inconsistent data?
The answer may be a race condition.
Race conditions happen when multiple operations access or modify shared state concurrently, and the final result depends on the order or timing in which those operations happen.

A race condition occurs when two requests interact with shared state at the same time and the application does not enforce safe ordering.
Why Race Conditions Matter
Modern web applications are inherently concurrent.
A single backend server may process hundreds or thousands of requests simultaneously. Those requests can:
Read and update the same database records
- read and update the same database records
- create related records
- modify inventory
- process payments
- update employee information
- generate reports
- execute background jobs
- trigger webhooks
- update cached values
Consider a simple account balance:
Current balance = $100
Request A: Read balance → $100Request B: Read balance → $100
Request A: Withdraw $80Request B: Withdraw $50
Request A: Save balance → $20Request B: Save balance → $50
Expected result:
The system should reject one withdrawal.
But without proper synchronization, both operations can act on the same original balance.

This type of lost-update behavior is a classic example of concurrent operations accessing shared state without sufficient coordination.
What Is a Race Condition?
A race condition occurs when the correctness of a program depends on the timing or ordering of concurrent operations.
A simplified example looks like this:
const currentValue = await getValue();const newValue = currentValue + 1;await saveValue(newValue);
Now imagine two requests execute this code simultaneously.
| Request A | Request B |
|---|---|
| Read value = 10 | Read value = 10 |
| Calculate 11 | Calculate 11 |
| Save 11 | Save 11 |

Both requests successfully executed the code, but one update effectively overwrote the other.
- Expected result: 12
- Actual result: 11
This is commonly called a lost update.

Inventory Race Conditions
Inventory systems are another classic example.
Suppose available stock = 1
Two customers purchase the final item.
const product = await Product.findByPk(productId);if (product.stock > 0) {await product.update({stock: product.stock - 1});}
Both requests may read:
stock = 1
Both pass:
product.stock > 0
And both attempt to purchase the item.

A safer approach is an atomic database update:
UPDATE productsSET stock = stock - 1WHERE id = ?AND stock > 0;
Then check how many rows were updated:
- 1 row updated → Purchase allowed
- 0 rows updated → Out of stock
The important difference is that the condition and update happen as one database operation.
Transactions and Race Conditions
Transactions are useful when multiple database operations must succeed or fail together.
For example:
await sequelize.transaction(async (transaction) => {const order = await Order.create(orderData,{ transaction });await Payment.create(paymentData,{ transaction });await Inventory.update(inventoryData,{ transaction });});

A transaction provides an important foundation for consistency, but transactions alone do not automatically solve every race condition. The isolation level, queries, locks, and application logic still matter.

Row-level locking can serialize conflicting updates, although locks should be kept as short as practical because excessive contention can hurt performance.
Idempotency and Duplicate Requests
Another important solution is idempotency.
Imagine a user clicks Pay Now and the request reaches the server, but the response is lost because of a network problem. The client retries: request 1 is a payment, request 2 is a payment retry. Without idempotency, the backend might process both requests.
Pay Now
and the request reaches the server, but the response is lost because of a network problem.
Let's Build Something Great Together
Ready to transform your idea into a powerful software solution? Talk to our experts and get a free consultation.
Contact UsThe client retries:
Request 1 → Payment
Request 2 → Payment retry
Without idempotency, the backend might process both requests.

With an idempotency key:

This makes retries much safer because the server can recognize that the same logical operation has already been processed.
The same principle applies to event-driven systems, where duplicate delivery is a normal part of operation. Consumers must stay idempotent by design to avoid processing the same event twice.
Background Jobs and Race Conditions
Race conditions also occur with background workers.
Imagine a job:
Job ID: 123
Status: pending
Two workers pick it up:
Worker A → Reads pending
Worker B → Reads pending
Worker A → Sends email
Worker B → Sends email
The customer receives two emails.

A common solution is to atomically claim the job:
pending
↓
processing
↓
completed
Only one worker should successfully transition the record from pending to processing.
For Example:
UPDATE jobsSET status = 'processing'WHERE id = ?AND status = 'pending';
- 1 row updated → Worker successfully claimed job
- 0 rows updated → Another worker already claimed it
Race Conditions in Distributed Systems
The problem becomes even more challenging when an application has multiple servers.


A local JavaScript variable or process-level lock only protects one application instance.
It does not coordinate:
server A
vs
server B
vs
server C
For distributed applications, coordination generally needs to happen through shared infrastructure such as:
- database transactions
- database locks
- unique constraints
- distributed locks
- message queues
- idempotency records
- atomic cache operations
Message queues and event buses bring their own duplication risks. Our guide to asynchronous communication with Amazon EventBridge covers idempotent consumers and retry handling in event-driven architectures.
The Five Questions to Ask
When reviewing backend code, ask:
- 1What state is shared?
- 2Can two requests access it simultaneously?
- 3Is there a gap between reading and modifying it?
- 4What happens if the operation runs twice?
- 5Which layer guarantees correctness?
This is a particularly useful checklist during code reviews.
It pairs well with QA and testing services that validate behavior under real concurrency, not just happy-path test data.
Conclusion
Race conditions are not usually caused by complicated algorithms. They often come from simple code interacting with shared state at the wrong time.
The goal is not to eliminate concurrency. The goal is to make concurrent operations safe and deterministic.
- Use database constraints for uniqueness.
- Use atomic updates when operations can be expressed as a single database action.
- Use transactions when multiple changes must succeed together.
- Use locking when concurrent access must be serialized.
- Use optimistic locking when conflicts should be detected rather than blocked.
- Use idempotency when requests may be retried.
Most importantly, design around state transitions and invariants, not just individual functions.
A good concurrent application should produce the correct result whether one request arrives or one hundred requests arrive at nearly the same time.
The question is not:
"Will these requests run at the same time?"
The better question is:
"If these requests run at the same time, is my system still guaranteed to produce the correct result?"
That is the difference between code that works in a normal test and software that remains reliable under real-world concurrency. If you are building custom software that has to stay correct under concurrent load, this is the mindset that matters.


10 AI Prompts Every Manual Tester Should Have in Their QA Toolkit
Discover 10 practical AI prompts manual testers can use to improve test coverage, find edge cases, analyze bugs, strengthen regression testing, and become more effective QA engineers.
Read More
The Website Accessibility Revolution: Why You Can't Afford to Ignore WCAG 2.2
Website accessibility starts with WCAG 2.2. Learn how accessible design, development, QA, and testing can help create websites that work for everyone.
Read More
AI Writes the Code • Stop Blindly Shipping It Without Reviewing the Engineering
A practical perspective on AI-generated code, scalability, maintainability, and engineering judgment and why engineering review still owns the final decision.
Read More
Migrating Databases with AWS DMS With Nearly Zero Downtime
Learn how to migrate databases using AWS DMS with nearly zero downtime using Full Load, CDC, validation, monitoring, and controlled cutover.
Read More
How to Add LiveKit Video Calling to a Next.js App
Add embedded video & audio calling to Next.js with LiveKit Cloud. Compared vs Twilio, Daily, Agora, Zoom — plus token auth, guests & recording.
Read More
We chose ECS over EKS: what we gained and what we gave up
An honest comparison of ECS vs EKS the costs, tradeoffs, and real-world reasoning behind choosing ECS for a production platform on AWS.
Read More
Upgrading Legacy Systems: From Outdated Technology to Competitive Advantage
Learn how to upgrade legacy systems through application modernization, API integration, cloud migration, security improvements, and incremental system upgrades without disrupting business operations.
Read More
Building Distributed Tracing and Observability with AWS X-Ray
A practical guide to correlating requests across a multi-tier application using correlation IDs, AWS X-Ray segments, and structured logging for faster incident debugging.
Read More
Designing Before and After AI: What Really Changed
A look at how AI has transformed UI/UX design from manual wireframes and slow research to AI-assisted prototyping, design-to-code, and personalization at scale.
Read More