Recognizing the pattern, and the toolkit
coreadvancedThe classic race: two requests both READ balance=100, both independently decide to write, and the second write overwrites the first — a lost update, not a crash, which is exactly why it is dangerous (nothing fails loudly). Every fix shares the same shape: remove the window between reading and writing, either by never reading at all (F()), by locking the row for the duration of the decision (select_for_update()), or by making the database itself reject the bad outcome (a unique/check constraint) rather than trusting application code to avoid it.
Think of it as
A race condition is not about speed — it is about a WINDOW between "read a value" and "write based on it" during which another process can act on the same, now-stale, information. Request A reads balance=100, request B reads balance=100 (same identical value, because A has not written yet), A computes and writes 60, B computes and writes 40 — the correct answer (100 minus both changes) never gets written, because B's calculation was based on a value that was already stale by the time B wrote. Every fix in the toolkit closes this window a different way: F() removes the read step entirely (the database computes the new value from its own current value, atomically); select_for_update() keeps the read but locks the row so no other transaction can read-and-decide during the window; a unique/check constraint does not prevent the race at all — it lets the race happen, but makes the DATABASE refuse the outcome that would violate the rule, converting a silent lost update into a loud, catchable error.
What we're doing: Recognize a balance-update race in existing code, and choose the right fix based on whether the operation actually needs to read-and-decide, or is a pure unconditional change.
- 2
- This read has no protection at all — another concurrent withdraw() call for the same account can read the same starting balance before this one writes.
- 3
- The actual bug is here — the new balance is computed from a value that is only correct at the moment it was read, not necessarily at the moment it gets written.
Why this works: This function genuinely needs a DECISION (does the account have sufficient funds? that check needs the current balance) — so the correct fix is select_for_update() (lock the row, check, then write), not a plain F() expression, since F() has no way to express "only subtract if the resulting balance would be non-negative."
Assuming any function that reads then writes a value automatically has a race condition, even when Python-level concurrency (not database concurrency) is the actual environment
Wrong
Better
What you see: Not a bug — unnecessary complexity and lock overhead added to code that never actually runs under any concurrent access at all, based on a reflexive "reads then writes, must need locking" pattern-match rather than an actual analysis of the deployment context.
Why: A race condition requires an actual SECOND concurrent actor capable of reading/writing the same data during the window — a one-off, single-run management script with no other process touching the same rows has no race to protect against, and select_for_update() there only adds lock overhead and complexity for a scenario that cannot occur. Recognizing the pattern means checking whether a genuine concurrent writer exists, not applying the fix reflexively to every read-then-write shape.
- Request A → balance row: read balance=100
- Request B → balance row: read balance=100
- Request A → balance row: write 60 (100-40)
- Request B → balance row: write 40 (100-60) — overwrites A
The concurrency toolkit, mapped to where it is covered in depth
Together
Remember: A race condition is a silent lost update from a window between reading a value and writing based on it — not a crash, which is why it is dangerous. F() removes the read step entirely (best when no decision is needed); select_for_update() locks the row for a genuine read-then-decide need; a unique/check constraint lets the database reject a bad outcome after the fact. Match the specific tool to the specific race — they are not interchangeable.
See also: idempotency and conditional updates · use cases and limits · f and q expressions

