What Is a Race Condition?

A race condition is a flaw that appears when the correctness of a program depends on the timing or interleaving of two or more operations that run concurrently. When those operations touch the same data and at least one of them writes to it, the final result can change depending on which operation happens to run first.

Race conditions are among the hardest bugs to diagnose because they are nondeterministic: the same code may produce the right answer thousands of times and then silently produce a wrong one. This guide explains what a race condition is, how one actually arises, why it matters, and the established techniques used to prevent it.

What a race condition actually is

The word "race" is literal. Two or more flows of execution are racing to access a shared resource, and the program's behavior changes depending on who wins. The flows might be operating-system threads, async tasks on a single thread, separate processes, or even independent HTTP requests hitting the same database row.

The classic illustration is incrementing a shared counter. The statement count = count + 1 looks atomic, but at the machine level it is usually three separate steps: read the current value, add one, write the result back. If two threads run those three steps at the same time, both can read the same starting value, both add one, and both write back the same number. Two increments happened, but the counter only went up by one. That lost update is a race condition.

The critical section

The span of code that accesses shared mutable state is called the critical section. A race condition is fundamentally a critical section that is not protected: more than one flow can be inside it at once. The fix, in every case, is to make sure that access to that shared state is coordinated so the operations cannot interleave in a harmful way.

How a race condition arises

Three ingredients must all be present for a race condition to exist. Remove any one of them and the bug cannot occur:

  • Shared state. Two flows reference the same memory, file, row, or cache entry.
  • Concurrent access. The flows can run at overlapping times, whether through true parallelism on multiple cores or interleaving on one core.
  • At least one mutation. If every flow only reads and nobody writes, ordering does not matter and there is no race.

A particularly common variant is the check-then-act race, also called time-of-check to time-of-use (TOCTOU). Code checks a condition, then acts on the assumption that the condition still holds. Between the check and the act, another flow changes the world. For example, "if the file does not exist, create it" can fail when two processes both pass the check before either creates the file. The same pattern appears as "if the account balance is sufficient, withdraw" in finance code and as duplicate-record creation in web backends.

Why race conditions matter

The consequences range from cosmetic to catastrophic. A miscounted analytics metric is annoying. A double-charged payment, a withdrawn balance that goes negative, a corrupted file, or two users assigned the same supposedly unique identifier are far more serious. In multithreaded systems, races can also corrupt internal data structures and crash the process outright.

Security is a major reason to care. TOCTOU races are a recognized class of vulnerability: an attacker who can change a resource in the gap between check and use may bypass a permission check or escalate privileges. Concurrency bugs in authentication and authorization logic are a frequent source of real exploits, which is one reason rate limiting and careful state handling matter on any public endpoint. If you build APIs, the same discipline that prevents races also supports sound rate-limiting strategies.

When you will encounter them

Race conditions show up anywhere concurrency exists, which today is almost everywhere:

  • Multithreaded programs sharing objects, collections, or counters across threads.
  • Async and event-loop code where an await point lets another task run and mutate shared state before the first task resumes. Single-threaded does not mean race-free.
  • Web backends handling simultaneous requests that read and write the same database row, such as an inventory count or a wallet balance.
  • Distributed systems where multiple services or replicas update shared data; a webhook that can be delivered more than once is a textbook trigger for duplicate processing.
  • File systems and caches, where multiple writers race over the same path or key.

How to prevent race conditions

There is no single fix; the right tool depends on the shape of the problem. The table below compares the established approaches.

TechniqueHow it worksBest for
Mutex / lockOnly one flow may hold the lock and enter the critical section at a timeGeneral mutable shared state in one process
Atomic operationHardware-level read-modify-write that cannot be interruptedSimple counters and flags
ImmutabilityData is never mutated after creation, so there is nothing to race overFunctional designs, shared read-only data
ConfinementState is owned by one flow; others communicate via messages or a queueActor models, worker pipelines
TransactionsDatabase isolation makes a group of operations all-or-nothingWeb backends and shared rows

Locks and atomics

A mutex (mutual exclusion lock) is the workhorse. Surround the critical section so only one flow runs it at a time. The cost is reduced parallelism and the risk of deadlock if two flows wait on each other's locks. For the narrow case of a single counter or flag, an atomic operation is cheaper, because the processor guarantees the read-modify-write completes as one indivisible step.

Eliminate the shared state

The most robust fix is often to remove a precondition rather than add a lock. If data is immutable, no write can race a read. If state is confined to a single owner and other flows send it messages, there is no shared mutable memory at all. This is the philosophy behind message-passing concurrency.

Push coordination to the database

For web applications, the database is usually the right place to enforce correctness. Wrap related reads and writes in a transaction, use a unique constraint so duplicate inserts fail loudly instead of racing, or use atomic SQL such as UPDATE accounts SET balance = balance - 10 WHERE id = 1 AND balance >= 10 so the check and the act happen as one statement.

Detecting and reasoning about races

Because races are intermittent, ordinary testing rarely catches them; a test can pass a thousand times by luck. Useful tactics include stress testing with many concurrent workers, dedicated race detectors (the Go race detector and ThreadSanitizer for C and C++ are well-established examples), and disciplined code review of every critical section.

When you suspect a race, capturing and comparing two divergent runs side by side can reveal the interleaving; a tool such as a diff checker helps spot where the outputs differ. If concurrent processes write structured logs, formatting them with a JSON formatter makes the ordering of events far easier to follow. For the conceptual foundations of concurrent systems and the protocols beneath them, our explainer on TCP vs UDP is a useful companion.

Common pitfalls

  • Assuming single-threaded means safe. Async code yields at every await, so shared state can still be mutated between steps.
  • Locking too little. Guarding the write but not the matching read leaves the check-then-act gap wide open.
  • Locking too much. Coarse locks serialize everything and can introduce deadlocks; over-locking trades a correctness bug for a performance or liveness bug.
  • Trusting "it works on my machine." Races surface under load, on faster hardware, or with more cores. Passing tests are not proof of safety.
  • Ignoring idempotency. In distributed systems, retries and duplicate deliveries are normal; design operations so running them twice is harmless.

The throughline is simple: identify every critical section, decide how access to it is coordinated, and prefer designs that remove shared mutable state entirely. Most race conditions are not exotic concurrency theory; they are unprotected critical sections waiting for the unlucky interleaving that production will eventually deliver.

Frequently Asked Questions

It is a bug where two or more parts of a program run at the same time and touch the same data, and the result depends on which one happens to finish first. Because that ordering is unpredictable, the program sometimes produces the wrong answer.

Yes. Async and event-loop code is single-threaded but still interleaves tasks. Every await or yield point is a chance for another task to run and change shared state before the first task resumes, which is enough to create a race.

A race condition is unprotected concurrent access that can produce wrong results. A deadlock is when two or more flows each wait for a lock the other holds, so none can proceed. Deadlocks are sometimes introduced while trying to fix races with locks.

Coordinate access to shared mutable state. Use a mutex or atomic operation in-process, wrap related database reads and writes in a transaction, add unique constraints, or eliminate the shared state through immutability or message passing.

They are nondeterministic. The harmful interleaving may occur only under heavy load or specific timing, so code can pass tests thousands of times and still fail in production. Race detectors and stress testing help, but ordinary unit tests rarely catch them.