computers/race-conditions-why-two-people-adding-1-to-5-can-leave-you-with-6.md
Race conditions: why two people adding 1 to 5 can leave you with 6
Two phones add one item to a shared list of 5, and the list ends up at 6 instead of 7. Nothing is broken, yet an update was lost. This is a race condition: adding 1 is really three steps (read, add, write), and two workers can slip into the gap between them.
Play the scheduler yourself, press the buttons in different orders, and see why the answer changes. Then switch on a lock and watch the bug disappear. Includes Python code we ran, with real output.

Two people open the same shared shopping list on their phones. The list says 5 items. Each person adds one item and taps save. You would expect the list to say 7. Sometimes it says 6, and nobody did anything wrong. This is a race condition, and it is one of the most famous bugs in programming.
One line of code is really three steps
In most languages, adding one to a number that lives in shared memory looks like a single action: counter += 1. A computer does not do it in one go. The processor can only change numbers it is holding in its own tiny private storage (a register), so it does three separate things:
- Read the number from shared memory into a private copy.
- Add 1 to the private copy.
- Write the private copy back to shared memory.
You can see this in Python, which can print the steps it takes for a function (Python 3.13 shown, other versions word it slightly differently):
import dis
counter = 0
def add_one():
global counter
counter += 1
dis.dis(add_one)
# LOAD_GLOBAL counter <- read
# LOAD_CONST 1
# BINARY_OP += <- add
# STORE_GLOBAL counter <- write
Notice the gap between the read and the write. If only one piece of code runs, the gap is harmless. If two pieces of code run at the same time, or take turns very quickly (these are called threads, or separate programs sharing a database), something can happen inside that gap.
Try it: you are the scheduler
Below, workers A and B each want to add 1 to a shared counter that starts at 5. The right answer is 7. A computer decides who runs next, and you cannot predict its choice. So here you choose by pressing the buttons. Try pressing A, B, A, B, A, B, then try finishing A completely before starting B.
What went wrong
If you pressed A, B, A, B, A, B, both workers read 5 before either wrote anything. Each added 1 to its own private copy and got 6. Then A wrote 6, and B wrote 6. Two additions happened, but the counter only went up by one: B's write overwrote A's work with a number that was already out of date. This is called a lost update.
The same run as a small Python program, where each worker pauses after every step (a yield hands control back so we can pick who goes next):
counter = 5
def worker(name):
global counter
seen = counter # step 1: READ
yield f"{name} read {seen}"
seen = seen + 1 # step 2: ADD
yield f"{name} computed {seen}"
counter = seen # step 3: WRITE
yield f"{name} wrote {seen}"
a, b = worker("A"), worker("B")
for turn in [a, b, a, b, a, b]:
print(next(turn))
print("counter =", counter, "(expected 7)")
Output when we ran it:
A read 5
B read 5
A computed 6
B computed 6
A wrote 6
B wrote 6
counter = 6 (expected 7)
Change the order to [a, a, a, b, b, b] and you get 7. The code is identical. Only the timing changed, and that is the whole problem: the bug depends on luck.
Why this bug is so annoying
A race condition does not happen every time. It needs the unlucky timing, which might occur once in a thousand runs, or only on a busy server, or only on someone else's faster computer. Your tests can pass a hundred times and the bug can still be there. Adding a print statement to investigate can even change the timing and hide it. That is why people say these bugs "disappear when you look at them".
It is also not only about threads in one program. The shopping list example is the same bug across a network: two phones read the list, each adds locally, and each writes back its own total. Databases and web servers face this all the time.
The fix: take turns with a lock
The cure is to make the three steps behave as one step that nobody can interrupt. The usual tool is a lock (also called a mutex, short for "mutual exclusion"). A worker takes the lock before it reads, and gives it back after it writes. While one worker holds the lock, the other must wait. Switch on "Use a lock" in the demo above and reset: the second worker's button now says it is waiting until the first one is finished, and the answer is always 7.
In real Python it looks like this. Two threads each add 1 to the counter 100,000 times, with a lock around the update:
import threading
counter = 0
lock = threading.Lock()
def work():
global counter
for _ in range(100_000):
with lock:
counter += 1
threads = [threading.Thread(target=work) for _ in range(2)]
for t in threads: t.start()
for t in threads: t.join()
print(counter) # 200000
We ran this and it printed 200000. The with lock: line means "wait for my turn, run this indented block alone, then let the next one in". Without the lock, whether a real run loses updates depends on your Python version and timing, which is exactly the point: you cannot rely on getting lucky.
What to take away
Whenever two things can change the same data, ask: is there a gap between reading it and writing it back? If yes, someone else can slip into that gap. You can then protect the steps with a lock, or avoid sharing in the first place (give each worker its own data and combine the results at the end), or use a tool that does the update as one indivisible step. Databases offer this too: an update such as UPDATE items SET count = count + 1 lets the database do the read and write as one operation, instead of your code reading and writing separately. Locks have their own trap, though: if two workers each hold a lock the other needs, both wait forever (a deadlock), a story for another day.