A single write command against a single document is atomic, on a standalone server as well as a replica set. The guarantee covers the whole update: every operator in it, however many fields and array elements it touches, is applied under one exclusive lock on that document. No reader sees half of it, no concurrent writer interleaves. That matters because the document model lets you keep related data together — a cart with its line items, an order with its address — so one atomic write often does the job. Ask that design question first (Schema Design).
const c = client.db('bank').collection('counters');
await c.insertOne({ _id: 'hits', n: 0, log: [] });
await Promise.all(Array.from({ length: 500 }, () =>
c.updateOne({ _id: 'hits' }, { $inc: { n: 1 } })));
console.log((await c.findOne({ _id: 'hits' })).n);
await c.updateOne({ _id: 'hits' }, { $inc: { n: 10 }, // all three land together
$push: { log: 'batch' }, $set: { at: new Date('2026-09-22') } });
console.log(JSON.stringify(await c.findOne({ _id: 'hits' })));
console.log(JSON.stringify(await c.findOneAndUpdate({ _id: 'hits' },
{ $inc: { n: 1 } }, { returnDocument: 'after', projection: { n: 1 } })));500
{"_id":"hits","n":510,"log":["batch"],"at":"2026-09-22T00:00:00.000Z"}
{"_id":"hits","n":511}Five hundred parallel increments produced exactly 500, in 446 ms, because $inc is applied by the server inside the document lock instead of being a read-modify-write in your application. The application-side version — read n, add one, write it back — loses updates at this concurrency, and is the commonest reason people reach for a transaction they do not need. findOneAndUpdate extends that to reads, returning the value its own write made.
What is not atomic is a sequence of commands. Two updateOne calls, or a bulkWrite with ordered: true — each operation is atomic, the group is not. A reader can land between them, and a crash can leave the first applied and the second not: bulkWrite batches round trips, not commits.