What is a race condition — the bug that shows up only sometimes

A beginner's most infuriating bug is the one that happens every other time. You press the button — works. Press again — broken. Try to show a friend — works again. Start catching it step by step — it disappears. Behind bugs like these there's often one and the same thing: a race condition.
The name is honest: two operations literally race for the same resource, and who gets there first isn't known in advance. Sometimes the order is accidentally right, and all is well. Sometimes it isn't — and the bug pops out. In a couple of minutes you'll understand where this "now it works, now it doesn't" comes from.
What a race condition is, in plain words
A race condition is when the result depends on the order in which two actions fired, and that order isn't guaranteed. The program assumed the steps would go one after another. Instead they overlapped.
This happens because modern code often doesn't wait: while one operation drags on (a request to a server, a read from a database), the program takes up the next one. Usually that's a blessing — the interface doesn't freeze. But if two such operations touch the same thing, they can step on each other's toes.
An analogy: the two of you are editing one paper list. You pick up the sheet, add a line, put it back. At the same moment your friend grabbed the old copy, added their own, and laid it over yours. Your line is gone — it got overwritten. Nobody made a mistake. You just worked on a shared sheet without taking turns.
Why the bug "comes and goes"
Let's take the classic — a counter. Two requests want to bump a number by 1 at the same time. Inside, each does three steps:
- read the current value (it was 100);
- add 1 (now 101);
- write it back.
If the requests go in turn, you get 102 — as intended. But if they overlap: both read 100, both add 1, both write 101. One increment is lost. It should have become 102, but it became 101.
Now it's clear why the bug is elusive. It all depends on micro-timing — a difference of a fraction of a millisecond. On your computer the requests almost always manage to separate, and the bug sleeps. Under load, when there are many users, overlaps happen constantly — and it wakes up. Hence the "works locally, glitches in production."
Where it hurts in practice: two clicks on "Pay" — and the money is charged twice. Two processes write to one file — you get a mess. AI wrote code that hits the database from several places at once — and the data drifts apart. All these bugs are relatives.
How to avoid it
The general idea is one: don't let two operations touch a shared resource any which way. Impose a queue. In practice that's a few techniques:
- Make the operation indivisible (atomic). Many databases can "add 1" with a single command that can't be interrupted midway. Then the "read — add — write" steps don't spread apart, and there's no race.
- Disable the button after a click. The most common interface race. Someone pressed "Pay" — immediately gray out the button until the response comes back. No double charge.
- Check whether it's already done. Before creating an order, make sure one doesn't exist yet. That saves you from duplicates even if the request accidentally went out twice.
And the first step to a fix is to reproduce the bug. Since it depends on timing, catch it under load: run the action many times in a row or in parallel. And to see what order things fired in, turn on detailed logs with timestamps — they show the overlap you can't see with your eyes.
FAQ
How is a race condition different from an ordinary bug?
An ordinary bug reproduces: same actions — same error. A race is inconsistent — it depends on which operation happened to run first, and that changes from run to run. That's why it's so hard to catch: step-by-step debugging changes the timing, and the bug hides. For that inconsistency they nicknamed these "heisenbugs" — they vanish when you observe them.
Do races only happen with multithreading?
No. Classically — yes, when several threads run in parallel. But a beginner meets it more often in asynchronous code and on the web: two requests to a server, two clicks, two tabs of the same app. Technically it's one thread, but the operations still overlap in time — and the resource is shared.
Why doesn't it show up on my computer?
Because you have few simultaneous actions — operations almost always manage to separate. A race loves load: the more users and requests at once, the higher the chance of an overlap. The classic scenario is "flawless locally, fails every other time in production."
Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.





