Tuesday, November 3, 2026 Independent journalism
MediaChannel

technology

What is a race condition and how does it break software?

A race condition is one of the sneakiest bugs in software development, appearing only when two threads reach the same data at exactly the wrong moment. Here's what it is and how developers prevent it.

Organized network server cables for efficient data management.

Photo by panumas nikhomkhai on Pexels

A race condition is a bug that occurs when two or more threads or processes read and write shared data at the same time, and the final result depends on which one gets there first. The word "race" is deliberate: the threads are literally racing each other, and if the wrong one wins, the program produces corrupt data, a crash, or behaviour that's almost impossible to reproduce. Race conditions are responsible for some of the hardest bugs in computing history, precisely because they don't fail on every run.

Why timing is everything

Most software runs on processors that handle many tasks in parallel. A web server might handle 500 requests per second. A banking app might process dozens of transactions at once. Each of those tasks runs as a thread, sharing the same memory space. The problem starts when two threads read the same variable, each intends to update it, and the operating system switches between them at exactly the wrong point.

Consider a simple bank balance of $100. Thread A reads that value to apply a $50 withdrawal. Before Thread A writes the result back, Thread B also reads $100 to apply a separate $30 withdrawal. Both threads now write their results: Thread A writes $50, Thread B writes $70. Depending on which thread finishes last, the balance ends up either $50 or $70. Neither is correct. The actual balance should be $20. That's a race condition, and in a real financial system it's a disaster.

What makes race conditions so hard to find

The timing gap that causes the bug can be measured in nanoseconds. On most runs the threads finish in the right order and the software works fine. The bug only surfaces under specific conditions: high load, a particular CPU scheduling decision, or a slow disk read that creates just enough delay for the second thread to slip in. This is why race conditions are called Heisenbugs. They disappear or change behaviour the moment you try to observe them with a debugger, because attaching a debugger changes the timing.

It's also why code review alone won't catch them. The logic in each thread looks completely correct in isolation. The defect only exists in the relationship between threads.

How developers prevent race conditions

The most direct tool is a mutex (short for mutual exclusion), a locking mechanism that forces threads to take turns. Before a thread touches shared data, it acquires the lock. Every other thread that wants the same data waits until the lock is released. If you want a deeper look at how that mechanism works under the hood, what is a mutex and how does it stop software from breaking itself walks through the mechanics in full.

Three other techniques are worth knowing:

  • Atomic operations. Some processors provide instructions that read, modify, and write a value in a single uninterruptible step. Languages like C++ and Go expose these as atomic types. No lock is needed because the operation can't be interrupted mid-flight.
  • Immutable data. If a thread never modifies shared data but only reads it, there's no race. Functional programming languages lean on this idea heavily.
  • Message passing. Instead of sharing memory, threads communicate by sending messages to each other. Erlang and Go's goroutines are built around this model. There's no shared state to race over.

Race conditions in the real world

Race conditions aren't just a theoretical concern. The Therac-25 radiation therapy machine in the 1980s had a race condition in its control software that caused massive overdoses in at least six patients, three of whom died. The bug only appeared when a trained operator typed fast enough to trigger a timing window that slower operators never hit.

More recently, race conditions have been exploited as security vulnerabilities. A class of attack called a TOCTOU (time-of-check to time-of-use) vulnerability works by slipping a malicious file swap into the gap between when software checks a file's permissions and when it actually opens it. The check passes. The wrong file runs. Understanding these security edges connects directly to broader threats: what is a zero-day exploit and how does it work covers how undisclosed vulnerabilities of exactly this kind get weaponised before developers can respond.

How to detect race conditions in your own code

Several tools exist specifically to catch races. Google's ThreadSanitizer instruments compiled code and reports data races at runtime by tracking every memory access and flagging cases where two threads touch the same address without synchronisation. Go has ThreadSanitizer built directly into its compiler via the -race flag, making it available to any Go developer without extra setup.

Static analysis tools check code at compile time without running it. They're less precise than runtime tools but catch races before the program ever executes. For mission-critical systems, formal verification methods mathematically prove that certain interleavings of thread execution can't cause data corruption.

The connection to memory and performance

Locking is effective but has a cost. Every time a thread waits for a lock, it's idle. At scale, excessive locking creates bottlenecks that are nearly as damaging as the bugs you're trying to prevent. This is why modern concurrency design focuses on minimising the amount of shared mutable state, keeping critical sections as short as possible, and choosing the right tool (atomic, lock, or message-passing) for the situation at hand. Race conditions and the fixes for them sit at the intersection of correctness and performance, which is why concurrency is still one of the most demanding disciplines in software engineering.