Concurrency
Race Conditions
Read-modify-write in application code versus an atomic operator on the server.
A race is check-then-act in Node: two requests both see a state that is already gone by the time they write. Atomicity of updateOne does not help if the decision happened on a stale findOne.
const p = await db.products.findOne({ name: "Mechanical Keyboard" })
if (p.stock >= 1) {
await db.products.updateOne(
{ name: "Mechanical Keyboard" },
{ $set: { stock: p.stock - 1 } }
)
}const r = await db.products.updateOne(
{ name: "Mechanical Keyboard", stock: { $gte: 1 } },
{ $inc: { stock: -1 } }
)
if (r.matchedCount === 0) { /* sold out */ }| Race | Put the check in the write |
|---|---|
| Ship twice | { status: "pending" }, $set: shipped — second matchedCount 0 |
| Duplicate email | unique index; handle 11000 — not find then insert |
| Double redeem | { coupon: id, used: false }, $set: { used: true } |
| Stock | stock: { $gte: n }, $inc: -n |
matchedCount === 0 is the conflict signal for compare-and-swap without a version field: you named the expected state in the filter. Unique indexes are the insert-side version of the same idea. Transactions do not fix a find-then-set you left outside the session. The previous module is $inc; this lesson is stop reading to decide.
Interview question
How do you avoid a check-then-act race in MongoDB?
Do not findOne, branch in the app, then update by _id only. Put the condition in the update filter — status pending, stock gte 1 — and use $inc or $set in that same write. matchedCount 0 means I lost. For inserts, a unique index, not find-then-insert. That is compare-and-swap on the document; a transaction is a different problem (two documents).