replaceOne swaps the whole document for the one you hand it, keeping only _id; updateOne patches the fields you name. The two calls look alike and do very different things.
db.stock.insertOne({ _id: 1, sku: "A1", qty: 7, shelf: "3B" });
db.stock.updateOne({ _id: 1 }, { $set: { qty: 9 } });
console.log("updateOne ->", db.stock.findOne({ _id: 1 }));
db.stock.replaceOne({ _id: 1 }, { sku: "A1", qty: 9 });
console.log("replaceOne ->", db.stock.findOne({ _id: 1 }));updateOne -> { _id: 1, sku: 'A1', qty: 9, shelf: '3B' }
replaceOne -> { _id: 1, sku: 'A1', qty: 9 }shelf is gone. This is the bug behind a PUT /api/stock/:id route that passes req.body straight to replaceOne: any field the client's form omitted is deleted. Use replaceOne only when the request carries the complete document.
Upserting turns one call into "update it, or create it if it is not there". Add upsert: true and, when nothing matches, the server builds a document from the filter's equality clauses plus the operators.
const opts = { upsert: true };
const up = { $inc: { qty: 5 }, $setOnInsert: { createdAt: new Date("2026-09-22") } };
const r1 = db.stock.updateOne({ sku: "B2" }, up, opts);
console.log("first :", r1.matchedCount, r1.upsertedCount, r1.insertedId);
const r2 = db.stock.updateOne({ sku: "B2" }, up, opts);
console.log("second:", r2.matchedCount, r2.upsertedCount, r2.insertedId);
db.stock.findOne({ sku: "B2" }, { _id: 0 });first : 0 1 ObjectId('6ab1c8177b740c892da76fce')
second: 1 0 null
{ sku: 'B2', createdAt: ISODate('2026-09-22T00:00:00.000Z'), qty: 10 }Run one: nothing matched, so the server inserted { sku: "B2" } from the filter, applied $inc to a missing field (which starts at zero) and ran $setOnInsert. Run two: the document existed, qty went to 10, and $setOnInsert was skipped — which is how you keep a createdAt that never moves. mongosh 403 reports the new key as insertedId; the Node driver 7.6.0 calls it upsertedId.
Upserts under concurrency need a unique index on the filter's fields. Two clients can both find no match and both insert; with the index one of them gets the duplicate key error of Inserting Documents and retries into the update path, and without it you get two documents where you wanted one.