Skip to content

Inserting documents

Inserting is the gentlest operation in MongoDB: you hand the database a document, it stores it, and it tells you what it stored. There is no schema to define first and no table columns to line up against - if the document is valid JSON-like data, it goes in. That freedom is exactly why a tiny bit of discipline around the _id field pays off, which is the one piece of bookkeeping every insert involves.

We keep building our imaginary library. Each member is a document, and in this lesson we add them to a collection called members. Every output below was captured on mongo 8.3.11 with mongosh 2.11.1 and Node driver 7.7.0 (Python, Go and Rust tabs were each run once in a scratch program on the same server); ids and timings will differ on your machine.

insertOne takes exactly one document and stores it. If you do not supply an _id, MongoDB generates one for you as an ObjectId - a 12-byte value that is unique across the collection and even encodes the creation time. That generated id is the primary key of the document.

mongosh/insert-one.js
const res = db.members.insertOne({ name: "Grace", joined: 2024, fines: 0 });
console.log(res);
console.log(db.members.findOne({ _id: res.insertedId }));
assert.strictEqual(res.acknowledged, true);

The acknowledgement tells you the operation succeeded and hands back the id that was assigned. What each tab really printed (ObjectIds are printed differently by every driver - the same 12 bytes):

mongosh { acknowledged: true, insertedId: ObjectId('6abca7761ca8500949f70bb3') }
Node { acknowledged: true, insertedId: new ObjectId('6abca7762e488e625dd180ed') }
Python 6abca733ec5eb1df4567945d
Go ObjectID("6abca735ba87132b4c842253")
Rust ObjectId("6abca7370cacf93463d138ad")

And the document now sitting in the collection (findOne in the shell) - the _id is the only thing you did not type yourself:

{
_id: ObjectId('6abca7761ca8500949f70bb3'),
name: 'Grace',
joined: 2024,
fines: 0
}

In Compass: open the members collection, use the Add Data drop-down and choose Insert Document (Compass documentation, fetched 2026-09-30, mongodb.com/docs/compass/current/documents/insert); pick Shell Syntax, Field-by-Field or JSON mode; if you do not specify _id, Compass generates an ObjectId.

When you have a batch of documents, insertMany sends them in one round trip instead of one call per document. You pass an array, and you get back the ids of everything that was inserted, keyed by their position in the array.

mongosh/insert-many.js
const res = db.members.insertMany([
{ name: "Linus", joined: 2023, fines: 2 },
{ name: "Margaret", joined: 2025, fines: 0 },
{ name: "Edsger", joined: 2022, fines: 5 }
]);
console.log(res);
assert.strictEqual(Object.keys(res.insertedIds).length, 3); // the shell result has no insertedCount

The acknowledgement maps each array index to the id that was generated for it. Two real differences worth knowing: the shell result carries no insertedCount (count the keys of insertedIds), while the Node result does:

mongosh
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6abca7761ca8500949f70bb4'),
'1': ObjectId('6abca7761ca8500949f70bb5'),
'2': ObjectId('6abca7761ca8500949f70bb6')
}
}
Node
{
acknowledged: true,
insertedCount: 3,
insertedIds: {
'0': new ObjectId('6abca77646b3ce4be9c0507f'),
'1': new ObjectId('6abca77646b3ce4be9c05080'),
'2': new ObjectId('6abca77646b3ce4be9c05081')
}
}
Python, Go, Rust: 3 (the length of the ids list, the number we sent)

_id must be unique. Insert a second document with an id that exists and you get a duplicate key error, code 11000, with the offending value in the message:

mongosh/insert-duplicate.js
db.members.drop();
const first = db.members.insertOne({ name: "Grace" });
let err;
try { db.members.insertOne({ _id: first.insertedId, name: "Impostor" }); } catch (e) { err = e; }
console.log(err.name, err.code);
console.log(err.message);
assert.strictEqual(err.code, 11000);
assert.strictEqual(db.members.countDocuments(), 1); // the original is untouched
MongoServerError 11000
E11000 duplicate key error collection: b1_run_insert.members index: _id_ dup key: { _id: ObjectId('6abca78db69a4386bb444676') }

The Node driver raises the same MongoServerError with code: 11000 and also exposes the failing key as err.keyValue ({ _id: new ObjectId('...') }).

insertMany is ordered by default: it stops at the first failing document. With { ordered: false } it tries every document and reports the failures at the end. We proved both with the same batch - document _id: 2 already exists, so the middle document of three collides:

mongosh/insert-ordered.js
db.members.drop();
db.members.insertOne({ _id: 2, name: "Existing" });
const batch = () => [{ _id: 1, name: "A" }, { _id: 2, name: "B (duplicate)" }, { _id: 3, name: "C" }];
let e1;
try { db.members.insertMany(batch()); } catch (e) { e1 = e; }
console.log("ordered :", e1.name, e1.code, "inserted", e1.result.insertedCount, "failed at index", e1.writeErrors[0].index);
console.log(db.members.find({}, { name: 1 }).sort({ _id: 1 }).toArray());
db.members.deleteMany({ _id: { $ne: 2 } });
let e2;
try { db.members.insertMany(batch(), { ordered: false }); } catch (e) { e2 = e; }
console.log("unordered:", e2.name, e2.code, "inserted", e2.result.insertedCount, "failed at index", e2.writeErrors.map((w) => w.index));
console.log(db.members.find({}, { name: 1 }).sort({ _id: 1 }).toArray());
assert.strictEqual(e1.result.insertedCount, 1);
assert.strictEqual(e2.result.insertedCount, 2);
ordered : MongoBulkWriteError 11000 inserted 1 failed at index 1
[ { _id: 1, name: 'A' }, { _id: 2, name: 'Existing' } ]
unordered: MongoBulkWriteError 11000 inserted 2 failed at index [ 1 ]
[
{ _id: 1, name: 'A' },
{ _id: 2, name: 'Existing' },
{ _id: 3, name: 'C' }
]

Ordered inserted 1 document (A), then stopped: C never got in. Unordered inserted 2 (A and C) and reported the single failure at index 1. Either way the error is a MongoBulkWriteError (code 11000), and the count of what did get in lives in err.result.insertedCount (in Node also err.insertedCount). Neither mode rolls back what it already inserted - there is no all-or-nothing here without a transaction.

  • You may supply your own _id (a string, number, or any value) instead of letting MongoDB generate an ObjectId - but it must be unique, or you get the E11000 error above.
  • insertMany is ordered by default: it stops at the first failing document, so documents after a duplicate-id error never get inserted. Pass { ordered: false } to keep going past failures and insert every document that does not collide.
  • Because there is no schema, two documents in the same collection can have completely different fields. Inserting is forgiving; that is a feature, not a bug - just stay consistent on purpose.

An assistant asked to “import the members safely” will often describe insertMany as atomic:

🐛 Spot the bug

// AI comment: "insertMany is all-or-nothing - if any member fails, none are saved"
async function importMembers(db, members) {
try {
await db.collection("members").insertMany(members);
return { ok: true };
} catch (err) {
return { ok: false, reason: err.message }; // safe: nothing was written
}
}
Show the answer

insertMany is not a transaction. In the real run above, with the middle document colliding, the first document was already committed (inserted 1; A stayed in the collection) when the error came back, so the “nothing was written” assumption leaves a half-imported batch and a caller who believes the import failed cleanly. The fix is to inspect the partial result - or, when you truly need all-or-nothing, wrap the insert in a transaction (module 7). This fence reports what actually landed:

app/insert-partial-result.mjs
const c = db.collection("members");
await c.deleteMany({});
await c.insertOne({ _id: 2, name: "Existing" });
let landed = null;
try {
await c.insertMany([{ _id: 1, name: "A" }, { _id: 2, name: "B" }, { _id: 3, name: "C" }]);
} catch (err) {
landed = err.result.insertedCount; // honest report: how many made it in
}
console.log({ ok: false, landed });
if (landed !== 1) throw new Error("expected exactly the first document to have landed");
{ ok: false, landed: 1 }

An insert is a request to create a new primary key. Everything else is bookkeeping around that: the driver mints an ObjectId if you did not, the server enforces uniqueness of _id, and the acknowledgement echoes the ids back. A batch is just a sequence of independent inserts sent together - ordered decides whether the sequence stops at the first failure, not whether the whole batch is atomic.

ChoiceBenefitCost
insertOneSimple, straightforward per-document error handlingOne round trip per document - slow at volume
insertMany (ordered)A single batch, fewer round trips; deterministic stopping pointThe first document that errors stops the batch; earlier documents stay inserted, later ones never are (inserted 1)
insertMany (unordered)Keeps going past a failing document, inserting every document that does not collide (inserted 2)Insertion order is not guaranteed; you must inspect writeErrors to know which ones failed
insertMany inside a transactionAll-or-nothingNeeds a replica set; more overhead and a 60 s default lifetime
  • “insertMany is atomic.” - it is not; the ordered run above left the first document in place. Use a transaction when you need atomicity.
  • “Inserting one at a time in a loop is fine.” - wastes round trips; batch them with insertMany.
  • “insertOne returns the full inserted document.” - it returns acknowledged and insertedId. Use findOne by that _id to read the document.
  • “No error thrown means every document was inserted.” - on an unordered insertMany a MongoBulkWriteError is thrown, but code that swallows it misses the failures; check err.result.insertedCount and err.writeErrors.
  • “The shell and drivers return the same shape.” - the mongosh insertMany result has no insertedCount, the Node result has; ObjectIds print as ObjectId('..'), new ObjectId('..'), ObjectID("..") depending on the language.

Real-world example

The insertMany reference states the rule we measured: “Excluding write concern errors, ordered operations stop after an error, while unordered operations continue to process any remaining write operations in the queue”, and that insertMany() can be used inside distributed transactions (insertMany, consulted 2026-09-30).

Python/Go/Rust tabs mirror the verified shell/Node behaviour; each was run once in a scratch program against the same server (outputs above) but they are not executed by the course’s automated checker.

If you insert a document without an _id field, what happens?
By default, what does an ordered insertMany do when the second of three documents has a duplicate _id?
What does the acknowledgement of insertMany contain?
With { ordered: false } and the same batch (middle document duplicates), how many documents are inserted and where is the count?
Which error code identifies a duplicate key on insert?