Ordered vs Unordered Inserts
When you insert multiple documents at once with insertMany(), MongoDB has to decide what happens if one of those documents fails to insert — for example because it violates a unique index. By default, MongoDB stops at the first failure and abandons the rest of the batch. This is an ordered insert. But you can tell MongoDB to keep going and insert every document that can succeed, skipping only the ones that fail — an unordered insert. Choosing the right mode matters a lot for bulk data loads, imports, and any operation where partial success is acceptable (or not).
Overview: How Ordered and Unordered Inserts Work
insertMany() takes an array of documents and, internally, MongoDB doesn’t necessarily send them to the storage engine one-by-one in a single unbreakable transaction. Instead, it batches them and processes the batch according to the ordered option:
Ordered (the default, { ordered: true }): MongoDB inserts documents strictly in array order. As soon as one document fails — a duplicate _id, a schema validation failure, a data type error — MongoDB stops immediately. Every document before the failure is inserted; the failing document and everything after it in the array is not inserted, even if those later documents would have succeeded perfectly fine on their own.
Unordered ({ ordered: false }): MongoDB does not guarantee insertion order, and it does not stop on failure. It attempts every document in the batch, collects any errors along the way, and reports all of them together at the end. A document near the start failing does not block a document near the end from being inserted.
In both modes, when at least one document fails, insertMany() throws an error (in the shell and in the Node.js driver this is a MongoBulkWriteError/BulkWriteError). The error object carries useful detail: writeErrors (an array describing each failure, including the array index of the offending document), and a result summary telling you how many documents were actually inserted (commonly surfaced as insertedCount or, on the error’s result, nInserted). Reading this error properly is the difference between silently losing data during a bulk load and knowing exactly which records need to be fixed and retried.
It’s worth being clear about atomicity here: neither ordered nor unordered insertMany() is a single all-or-nothing transaction by default. Each individual document insert is atomic on its own, but the batch as a whole is not — some documents can succeed while others fail, in either mode. If you truly need all-or-nothing behavior across multiple documents, you need a multi-document transaction, which is a heavier tool reserved for cases where documents must succeed or fail together.
Unordered inserts also have a performance angle on sharded clusters: because MongoDB doesn’t have to preserve strict ordering, it can route and execute writes to different shards in parallel rather than waiting on each one sequentially. On a single replica set the difference is smaller, but it can still matter for very large batches.
Syntax
db.collection.insertMany(
[ <document1>, <document2>, ... ],
{
ordered: <boolean>,
writeConcern: <document>
}
);
| Parameter | Description |
|---|---|
[ <document1>, ... ] |
Required array of BSON documents to insert. Any document without an _id field gets one auto-generated as an ObjectId. |
ordered |
Optional boolean. Defaults to true. When true, MongoDB stops at the first write error. When false, MongoDB attempts to insert every document and reports all errors together. |
writeConcern |
Optional document controlling acknowledgement level (e.g. { w: "majority" }). Independent of ordered — it affects durability guarantees, not batch failure behavior. |
Examples
Example 1: Ordered insert stops at the first error
db.students.insertMany([
{ _id: 1, name: "Ava", grade: "A" },
{ _id: 2, name: "Ben", grade: "B" },
{ _id: 2, name: "Cleo", grade: "C" }, // duplicate _id, will fail
{ _id: 4, name: "Drew", grade: "B" }
]);
Output:
MongoBulkWriteError: E11000 duplicate key error collection: school.students index: _id_ dup key: { _id: 2 }
Result: { insertedCount: 2, insertedIds: { '0': 1, '1': 2 } }
Ava and Ben are inserted because they come before the failing document in the array. Cleo fails on the duplicate _id: 2. Because this is an ordered insert (the default), MongoDB never even attempts Drew — it stops the whole batch the moment Cleo fails. Running db.students.find() afterward would show only Ava and Ben, even though Drew’s document was perfectly valid.
Example 2: Unordered insert keeps going past the error
db.students.insertMany([
{ _id: 1, name: "Ava", grade: "A" },
{ _id: 2, name: "Ben", grade: "B" },
{ _id: 2, name: "Cleo", grade: "C" }, // duplicate _id, will fail
{ _id: 4, name: "Drew", grade: "B" }
], { ordered: false });
Output:
MongoBulkWriteError: E11000 duplicate key error collection: school.students index: _id_ dup key: { _id: 2 }
Result: { insertedCount: 3, insertedIds: { '0': 1, '1': 2, '3': 4 } }
This time insertedCount is 3: Ava, Ben, and Drew all made it in, and only Cleo failed. MongoDB attempted every document in the array regardless of the failure in the middle. This is the key behavioral difference — unordered mode maximizes how many documents get inserted, at the cost of not being able to assume the batch succeeded or failed as a unit.
Example 3: Handling a realistic bulk import with partial failures
const productBatch = [
{ sku: "SKU-100", name: "Wireless Mouse", price: 19.99 },
{ sku: "SKU-101", name: "Mechanical Keyboard", price: 79.99 },
{ sku: "SKU-100", name: "Duplicate Mouse Listing", price: 21.99 }, // sku has a unique index
{ sku: "SKU-102", name: "USB-C Hub", price: 34.5 }
];
try {
const result = await db.products.insertMany(productBatch, { ordered: false });
print(`Inserted ${result.insertedCount} products`);
} catch (err) {
print(`Inserted ${err.result.result.nInserted} products before hitting errors`);
err.writeErrors.forEach(we => {
print(`Row ${we.index} failed: ${we.errmsg}`);
});
}
Output:
Inserted 3 products before hitting errors
Row 2 failed: E11000 duplicate key error collection: shop.products index: sku_1 dup key: { sku: "SKU-100" }
This is the pattern you’ll actually use in application code: wrap the unordered insertMany() in a try/catch, because the promise rejects whenever any document fails, even though most of the batch succeeded. The catch block reads err.writeErrors to find out exactly which array indexes failed and why, so the calling code can log, alert, or retry just those records instead of treating the whole import as a failure.
How It Works Step by Step
When you call insertMany(), mongosh (or the driver) sends the array to the server as a single bulk write command. On the server:
1. MongoDB splits the array into batches internally if it’s very large (there are size and count limits per batch), but conceptually processes documents one at a time within the batch.
2. In ordered mode, the server processes document 0, then 1, then 2, and so on, stopping the instant one fails validation or a unique index check. Everything after that point in the array is never attempted, and the server reports the stop point back to the client.
3. In unordered mode, the server (and in a sharded cluster, potentially multiple shards concurrently) attempts every document independently, collecting a list of successes and a list of failures, then returns the full summary once the batch finishes.
4. Either way, each individual document insert is still atomic at the storage-engine level and still goes through any unique index checks and schema validation rules configured on the collection.
5. The client library turns a batch that contains any failures into a thrown error (MongoBulkWriteError), attaching the partial result so you can inspect what actually got written rather than assuming nothing did.
Common Mistakes
Mistake 1: Assuming a thrown error means nothing was inserted.
try {
await db.orders.insertMany(orderBatch, { ordered: false });
} catch (err) {
console.log("Import failed, nothing was saved"); // WRONG assumption
}
With ordered: false (and even partially with ordered: true), a thrown error usually means some documents were inserted successfully. Treating the whole batch as failed can cause you to re-insert documents that already exist, creating duplicates or triggering unrelated unique-index errors on retry. Always check err.result / err.insertedCount before deciding what to retry:
try {
await db.orders.insertMany(orderBatch, { ordered: false });
} catch (err) {
const failedIndexes = err.writeErrors.map(we => we.index);
const toRetry = orderBatch.filter((_, i) => failedIndexes.includes(i));
console.log(`${err.result.result.nInserted} saved, ${toRetry.length} need review`);
}
Mistake 2: Using the default ordered mode for large, independent bulk imports.
If you’re importing 50,000 CSV rows as documents and row #12 happens to violate a unique index, the default ordered behavior silently drops rows #12 through #50,000 — even though 49,987 of them were completely valid. For independent records where you want maximum throughput and don’t need strict order, use { ordered: false } and handle the reported failures separately, rather than losing the rest of a large valid batch to one bad row.
Best Practices
- Use
ordered: true(the default) when documents must be inserted in sequence and a failure partway through should stop everything — for example, a batch that depends on earlier documents existing first. - Use
ordered: falsefor bulk imports of independent records where you want to maximize successful inserts and handle failures separately. - Always wrap
insertMany()in atry/catchin application code — a partial failure throws even when most documents succeeded. - Inspect
err.writeErrors(array index + error message per failure) rather than assuming the whole batch failed. - Pre-validate obviously bad documents (missing required fields, wrong types) before sending the batch, so you don’t waste a round trip on failures you could catch client-side.
- Remember that neither mode is a transaction — if you need true all-or-nothing behavior across multiple documents, use a multi-document transaction instead.
Practice Exercises
- Create a
db.bookscollection with a unique index onisbn. Insert a batch of 5 books where the 3rd one has a duplicateisbn, once withordered: trueand once withordered: false. Compare how many documents end up in the collection each time. - Write a
try/catcharound an unorderedinsertMany()that logs exactly which array indexes failed and why, using the error’swriteErrorsarray. - Given a batch of 10 documents where documents at indexes 2, 5, and 7 are invalid, predict (then verify) the
insertedCountfor both ordered and unordered modes.
Summary
insertMany()defaults toordered: true: it inserts in array order and stops at the first failure, abandoning every document after it.{ ordered: false }attempts every document regardless of earlier failures and reports all errors together at the end.- Neither mode is a single atomic transaction across the batch — individual documents are atomic, but partial success across the batch is normal and expected.
- A failed
insertMany()throws aMongoBulkWriteError/BulkWriteErrorcarryingwriteErrorsand a partial insert count — always inspect it instead of assuming nothing was saved. - Prefer
ordered: falsefor large, independent bulk imports where maximizing successful inserts matters more than strict sequencing.
