Filter concepts by levelShowing all levels.

MongoDB · Atomicity and Concurrency

Atomicity and Concurrency

What single-document atomicity already gives you, and the patterns that close the gaps it leaves

  • One call = atomica read, then a separate write, is not
  • Optimistic concurrencystake the write on the value you expect, retry on conflict
  • Version fieldscatch any concurrent change, not just ones to one business field
  • Unique indexesthe only way to fully close a check-then-insert race

1.Single-document writes are atomic

This is the foundation the rest of this section builds on: a single write to a single document always applies completely or not at all, and no concurrent reader ever observes it half-done. Every concurrency pattern here — atomic operators, optimistic concurrency, compare-and-set — leans on this guarantee already existing.

Key features

  • Any single write call — updateOne, findOneAndUpdate, replaceOne — is atomic for the one document it targets
  • This is a per-write-call guarantee, not a per-logical-operation one — reading a value and then writing based on it is two operations, not one
  • It holds regardless of how many fields, nested objects, or array elements the update touches
  • It does not, by itself, prevent a "lost update" from two concurrent read-then-write sequences racing each other
  • Everything else in this section is a pattern for the gap this guarantee leaves: coordinating a decision based on a document's current value

Command

text
// Given: one write call to one document is always atomic. Ask: does my operation actually fit inside one call?

A single write call to one document is always atomic — but a read, then a decision, then a separate write is not one atomic operation, however fast it looks in testing.

2.Atomic operators instead of read-modify-write

When the new value can be computed from the old one using $inc, $push, $addToSet, or similar, use that operator directly instead of reading the document, computing the new value in application code, and writing it back. The operator form is one atomic step; the read-then-write form is two, with a race condition in between.

Key features

  • $inc/$mul for numeric changes relative to the current value — never read a number just to add to it
  • $push/$addToSet/$pull for array changes relative to the current array
  • $min/$max set a field only if the new value is smaller/larger than the current one, atomically, without a read
  • $currentDate sets a field to the server's current time atomically, avoiding a client-clock read
  • findOneAndUpdate can combine an atomic update with returning the resulting document in one round trip, when both are needed
Read-modify-write vs. an atomic operator

Read-modify-write

  • findOne() to read current value
  • compute new value in application code
  • updateOne() to write it back
  • race window between read and write

Atomic operator

  • updateOne() with $inc/$push/$max/…
  • one round trip, one atomic step
  • server computes the new value from the current one
  • no race window — nothing to race against
  • Read-modify-write
    • findOne() to read current value
    • compute new value in application code
    • updateOne() to write it back
    • race window between read and write
  • Atomic operator
    • updateOne() with $inc/$push/$max/…
    • one round trip, one atomic step
    • server computes the new value from the current one
    • no race window — nothing to race against

Command

text
$inc: { field: <delta> } · $push: { arr: <value> } · $max: { field: <value> } · $currentDate: { field: true }

If the new value is purely a function of the current one, use the matching operator ($inc, $push, $max, …) directly — reading it into application code first reopens the race condition atomicity was supposed to close.

3.Optimistic concurrency

Optimistic concurrency means proceeding as if no conflict will happen, then checking at write time — the update's filter includes the value you expect the document to currently hold, so the write only succeeds if nothing else changed it first. If it fails, you retry with fresh data instead of blocking.

Key features

  • The filter of the update carries the expected current state — a version number, a status value, or the field itself
  • matchedCount (or the equivalent in the driver) tells you whether the condition held — 0 means someone else got there first
  • On a failed match, the correct response is to retry the whole read-decide-write sequence with fresh data, not to force the write through
  • This scales well under low-to-moderate contention, because most attempts succeed on the first try with no blocking
  • Under very high contention on the same document, repeated retries can themselves become a bottleneck — that is a sign to reconsider the design, not just retry harder
Optimistic concurrency: stake the write on the value you read
Client
MongoDB
  1. 1read document, note version: 3
  2. 2updateOne({ _id, version: 3 }, { $set: {...}, $inc: { version: 1 } })
  3. 3matchedCount: 1 → success, version now 4
  4. 4(if matchedCount: 0 → someone else updated first, retry)
  1. Client → MongoDB: read document, note version: 3
  2. Client → MongoDB: updateOne({ _id, version: 3 }, { $set: {...}, $inc: { version: 1 } })
  3. MongoDB → Client: matchedCount: 1 → success, version now 4
  4. Client → MongoDB: (if matchedCount: 0 → someone else updated first, retry)

Command

text
updateOne({ _id, <expectedField>: <expectedValue> }, { $set: {...} }) — check matchedCount, retry on 0

Stake the write on the value you expect — put the expected current state in the update's filter, check whether it matched, and re-read before retrying on failure. That turns check-then-write into one atomic operation.

4.Version fields and compare-and-set

A dedicated version field (an integer, incremented on every update) is a more reliable condition for optimistic concurrency than checking a business field directly — it changes on every write, even ones that would not otherwise look different, so no update slips through unnoticed.

Key features

  • Add a version (or __v, common in some ODMs) field, starting at 0 or 1
  • Every update filters on the current version and increments it: { _id, version: v } → $set + $inc: { version: 1 }
  • This is the compare-and-set (CAS) pattern: compare the expected version, set the new state, atomically, in one call
  • A version field catches conflicts a business-field check would miss — two updates that both leave status unchanged, but touch other fields
  • A failed match (matchedCount 0) means the version moved — re-read and retry, the same response as any other optimistic concurrency conflict

Command

text
updateOne({ _id, version: v }, { $set: {...}, $inc: { version: 1 } })

A version field, incremented on every write and checked in the update filter, is the general-purpose compare-and-set pattern — more reliable than checking a business field directly, because it changes on every write, not just ones that touch that field.

5.Race conditions from stale reads

A stale read is a value read into application code that is no longer current by the time a decision based on it gets written back — the longer the gap between read and write, and the more concurrent writers there are, the more likely a decision ends up acting on outdated information.

Key features

  • The risk is proportional to two things: how long the gap between read and write is, and how much concurrent write traffic touches the same document
  • A stale read is not a bug in the read itself — it is a bug in trusting that value unconditionally at write time
  • Symptoms are often intermittent and load-dependent, which is why these bugs frequently pass code review and testing and surface only in production
  • The fix is never "read faster" — it is making the write itself conditional on the value still being current (optimistic concurrency), or eliminating the read entirely with an atomic operator
  • Business logic that spans an API call, a queue, or a user-facing multi-step form has an especially wide read-to-write gap and deserves extra scrutiny

Command

text
// Any code shaped like: read a value → decide something → write based on that decision, has a stale-read window to consider

A stale read is not the bug — trusting it unconditionally at write time is. The fix is a conditional write (optimistic concurrency) or an atomic operator, never "hope the gap stays small".

6.Unique indexes and atomic invariants

A unique index enforces "no two documents share this value" at the database level, atomically, for every insert regardless of race conditions — application-code checks like "look for an existing one first" cannot give the same guarantee, because the check and the insert are two separate steps that can race.

Key features

  • A unique index rejects an insert or update that would violate uniqueness, atomically, at write time — no read-then-check gap
  • Combined with an upsert, a unique index makes "insert if not exists" safe under concurrency without any application-side locking
  • A duplicate-key error (code 11000) is the expected, correct outcome of a race the unique index caught — not a bug to work around
  • Enforcing "at most one active X" with a unique index typically needs a partial index scoped to the active state, since a plain unique index applies to every document
  • Application-code existence checks ("findOne, then insert if nothing found") always have a race window — a unique index is the only way to close it completely

Command

text
db.collection.createIndex({ field: 1 }, { unique: true }) — then handle code 11000 as an expected outcome

A unique index enforces an invariant atomically for every writer, everywhere — an application-code existence check always has a race window a unique index does not.

Remember

  • One write call to one document is atomic — a read followed by a separate write is not.
  • If the new value is a function of the current one, use $inc/$push/$max/… directly.
  • Put the expected current value in the update filter to make check-and-write one atomic step.
  • A unique index closes a check-then-insert race that application code cannot close on its own.

Pro tip

  • A version field catches any concurrent change, even ones that leave a specific business field unchanged.
  • On a failed optimistic-concurrency match, re-read before retrying — retrying with the same stale value cannot succeed.

Next up

  • Read Concern and Write Concern
  • Sessions
  • Replication

Sources

  • Write operations atomicity — mongodb.com/docs/manual/core/write-operations-atomicity/
  • Unique indexes — mongodb.com/docs/manual/core/index-unique/
  • Checked 2026-09-10

Spotted a mistake on this page? Tell us.