Blog · Learn · 2026-09-05

What Is a Race Condition in Software? A Practical Guide

Klavity
TL;DRA race condition is a defect where a program's outcome depends on the relative timing of operations that run concurrently and share state without coordination. Because the interleaving varies run to run, the same code can work thousands of times and fail once. The durable fix is to make the shared operation atomic or serialized, not to add retries.

A race condition is a defect where a program's result depends on the relative timing or interleaving of operations that run concurrently — two or more threads, processes, requests, or asynchronous callbacks touching the same shared state without coordination. When those operations happen to interleave in the "wrong" order, you get a corrupted value, a lost update, a duplicated action, or a check that passes against data that has already changed. Because the ordering varies from one run to the next, the same code can work thousands of times and fail once — which is what makes race conditions some of the hardest bugs to reproduce.

What is a race condition, exactly?

The classic shape of a race condition is check-then-act or read-modify-write: a piece of code reads a shared value, makes a decision or a calculation based on it, then writes back — assuming nothing changed in between. When two flows run that sequence at the same time, one of them is deciding on stale data.

Consider two withdrawal requests hitting the same account balance of 100:

  1. Request A reads balance = 100. Request B reads balance = 100.
  2. Both check "is 100 >= 100?" — both pass.
  3. Both compute 100 − 100 = 0 and write 0.

The account is left at 0, but two withdrawals of 100 succeeded against a balance of 100. Neither request did anything wrong on its own; the bug lives in the gap between the read and the write. The same pattern produces lost counter increments, double-charged payments, duplicate records, and files created twice.

What causes race conditions?

Almost every race traces back to shared mutable state accessed without coordination. The common sources:

  • Non-atomic read-modify-write. Incrementing a counter, appending to a list, or updating a balance in separate read and write steps.
  • Check-then-act (TOCTOU). "Time of check to time of use" — you verify a file exists, a slot is free, or a username is available, then act on that assumption after it may have changed.
  • Unsynchronized threads or processes. Two workers mutating the same object, cache entry, or row without a lock.
  • Async ordering in a single thread. In JavaScript, two async flows that both read state, await something, then write back based on the value they read before awaiting.
  • Double-submit from the UI. A user double-clicks, or a slow network lets two identical requests overlap.
  • Missing transaction isolation. Concurrent database transactions reading each other's uncommitted or soon-to-change data.

How do you detect a race condition?

The tell is the symptom pattern, not the stack trace. Suspect a race when a bug is intermittent, load-dependent, order-dependent, or disappears the moment you try to observe it. A bug that vanishes when you add a log line or attach a debugger is called a heisenbug, and it points squarely at timing.

  1. Increase concurrency. Run the suspect path in a tight loop, in parallel, or under load. A race that fires once in 10,000 runs at rest often fires reliably under a burst of concurrent requests.
  2. Inject artificial delays. Add a small sleep between the read and the write in the code under test. Widening the window makes a rare interleaving happen almost every time — one of the fastest ways to confirm the hypothesis.
  3. Use a race detector. Language tooling can flag unsynchronized shared access directly: Go's -race flag, ThreadSanitizer for C/C++ and Rust, and Java's concurrency-stress tooling instrument memory access and report data races you'd never catch by eye.
  4. Log with timestamps and identifiers. Record a request or thread ID plus a high-resolution timestamp at each read and write. Interleaved entries from two IDs around the same value expose the overlap.

How do you fix a race condition?

Retrying, adding a delay, or "just try again" is not a fix — it only narrows the window. Durable fixes remove the unguarded gap between read and write:

  1. Make the operation atomic. Use an atomic increment, compare-and-swap, or a single database statement (UPDATE accounts SET balance = balance - 100 WHERE id = ? AND balance >= 100) so the read and write can't be split.
  2. Lock the critical section. Wrap the read-modify-write in a mutex or row lock so only one flow is inside it at a time. Keep the section as small as possible and always acquire multiple locks in the same order to avoid deadlocks.
  3. Push the guarantee into the database. A unique constraint prevents duplicate rows outright; SELECT ... FOR UPDATE serializes access to a row; optimistic locking (a version column checked on write) rejects an update built on stale data.
  4. Use idempotency keys. For double-submits and payments, attach a client-generated key so a repeated request is recognized and processed once, no matter how many times it arrives.
  5. Eliminate the shared state. Immutable data, a single-writer design, or serializing the work through a queue removes the contention instead of guarding it.

After the fix, lock it in with a test that runs the path concurrently — the same reproduction that exposed the race becomes the regression test that keeps it gone.

Why race conditions look like "cannot reproduce" bugs

Because a race depends on timing, a user hits it once and can't make it happen again on demand — so it lands in your tracker as a vague, low-confidence report that a developer marks cannot reproduce and closes. The information that would actually crack it — what else was happening at that instant, which requests overlapped, what the console and network showed — is gone the moment the tab closes.

That is exactly why capturing full state at the moment of failure matters more for timing bugs than for any other kind. A report that ships with the exact steps, the console log, the network waterfall, and precise timestamps gives you the interleaving to inspect instead of a guess to chase. Klavity's right-click bug reports attach that evidence automatically when a person flags the bug, so an intermittent failure arrives with the context a developer needs. For the reproduction workflow itself, see our guides on reproducing an intermittent bug and cutting "cannot reproduce" tickets.

Key takeaways

  • Suspect a race condition when a bug is intermittent, load-dependent, or vanishes under logging.
  • Reproduce it by adding artificial delays or hammering the code path concurrently, not by re-running once.
  • Fix the root cause: make the operation atomic, lock the critical section, or enforce it at the database.
  • Capture full state (steps, console, network, timestamps) at the moment of failure — retries won't teach you the interleaving.

FAQ

Is a race condition the same as a deadlock?

No. A race condition is when the result depends on unpredictable timing of concurrent operations, so you occasionally get the wrong answer. A deadlock is when two or more threads each hold a lock the other needs and both freeze forever. Ironically, adding locks to fix a race condition is a common way to introduce a deadlock, so add them in a consistent order and keep critical sections small.

Why does the bug disappear when I add logging?

Logging, breakpoints, or extra print statements change the timing of your code — they slow one thread just enough to hide the bad interleaving. A bug that vanishes when you observe it is called a heisenbug, and it's a strong signal you're looking at a race condition rather than a deterministic logic error.

Can single-threaded JavaScript have race conditions?

Yes. JavaScript runs one call stack at a time, but async operations (fetch, timers, promises) interleave between tasks. If two async flows both read shared state, await something, then write back based on the stale value they read, you have a race — even without threads. Double-clicking a submit button that fires two overlapping requests is the everyday version.

Catch bugs the moment a human sees them

Klavity: right-click bug reports, AI personas that review your product, and self-healing tests.

Get started free