Automattic/mongoose · error · MongooseBulkSaveIncompleteError
${modelName}.bulkSave() was not able to update ${numDocument
Error message
${modelName}.bulkSave() was not able to update ${numDocumentsNotUpdated} of the given documents due to incorrect version or optimistic concurrency, document ids: ${preview} What it means
bulkSave() issues one updateOne per changed document, filtered by _id AND the document's current __v so concurrent modifications are not overwritten (versioning/optimisticConcurrency). If the underlying bulkWrite succeeds but matchedCount + insertedCount is less than the number of documents, some version guards did not match and Mongoose throws MongooseBulkSaveIncompleteError, exposing .documents, .bulkWriteResult, and .numDocumentsNotUpdated. Any non-MongoBulkWriteError failure is rethrown as-is before this check.
Source
Thrown at lib/model.js:3650
}
await Promise.all(documents.map(doc => buildPreSavePromise(doc, options)));
const writeOperations = this.buildBulkWriteOperations(documents, options);
const opts = { skipValidation: true, _skipCastBulkWrite: true, ...options };
const { bulkWriteResult, bulkWriteError } = await this.bulkWrite(writeOperations, opts).then(
(res) => ({ bulkWriteResult: res, bulkWriteError: null }),
(err) => ({ bulkWriteResult: null, bulkWriteError: err })
);
// If not a MongoBulkWriteError, treat this as all documents failed to save.
if (bulkWriteError != null && bulkWriteError.name !== 'MongoBulkWriteError') {
throw bulkWriteError;
}
const matchedCount = bulkWriteResult?.matchedCount ?? 0;
const insertedCount = bulkWriteResult?.insertedCount ?? 0;
if (writeOperations.length > 0 && matchedCount + insertedCount < writeOperations.length && !bulkWriteError) {
throw new MongooseBulkSaveIncompleteError(
this.modelName,
documents,
bulkWriteResult
);
}
const successfulDocuments = [];
for (let i = 0; i < documents.length; i++) {
const document = documents[i];
const documentError = bulkWriteError?.writeErrors.find(writeError => {
const writeErrorDocumentId = writeError.err.op._id || writeError.err.op.q._id;
return writeErrorDocumentId.toString() === document._doc._id.toString();
});
if (documentError == null) {
successfulDocuments.push(document);
}
}View on GitHub (pinned to 49cdab0136)
Solutions
- Handle MongooseBulkSaveIncompleteError by re-fetching the affected documents (err.documents lists them) and reapplying/retrying the change, since the in-memory copies are stale
- Shorten the window: bulkSave soon after loading documents, avoid holding them across long awaits
- Give conflicting writers disjoint document sets (shard work by _id) so version guards never collide
- If last-write-wins is acceptable for the use case, use bulkWrite with plain updateOne ops (no version guard) or updateOne() directly instead of bulkSave
- Do not reuse the same document instances for a second bulkSave attempt after a concurrent modification; reload first
Example fix
// before
await Model.bulkSave(docs); // another worker already bumped __v on some docs -> throws
// after
try {
await Model.bulkSave(docs);
} catch (err) {
if (err.name !== 'MongooseBulkSaveIncompleteError') throw err;
const ids = err.documents.map(d => d._id);
const fresh = await Model.find({ _id: { $in: ids } }); // re-apply changes to fresh copies
for (const doc of fresh) applyChanges(doc);
await Model.bulkSave(fresh);
} Defensive patterns
Strategy: retry
Validate before calling
// Reduce the stale-version window before bulkSave
function assertFresh(Model, docs, toleranceMs = 5000) {
const stale = docs.filter(d => d.$__.saveTime != null && Date.now() - d.$__.saveTime > toleranceMs);
// practical version: track load time yourself
}
// Practical guard: reload right before saving
const fresh = await Model.find({ _id: { $in: docs.map(d => d._id) } }); Try / catch
async function bulkSaveWithRetry(Model, applyChanges, ids, attempts = 3) {
for (let i = 0; i < attempts; i++) {
const docs = await Model.find({ _id: { $in: ids } }); // fresh __v every attempt
applyChanges(docs);
try {
return await Model.bulkSave(docs);
} catch (err) {
if (err.name !== 'MongooseBulkSaveIncompleteError' || i === attempts - 1) throw err;
// err.documents / err.numDocumentsNotUpdated identify what missed
}
}
} Prevention
- Reload documents immediately before bulkSave instead of saving long-lived instances
- Partition concurrent workers by disjoint _id ranges so version guards never collide
- Monitor matchedCount vs documents sent when calling bulkWrite directly; bulkSave turns the gap into this throw
- Never retry bulkSave with the same stale instances; re-fetch so __v is current
When it happens
Trigger: Two processes bulkSave the same stale documents: one bumps __v, the other's version-guarded updateOne matches nothing; schemas with optimisticConcurrency: true where a document was revalidated/modified concurrently between load and bulkSave.
Common situations: Race conditions between workers/API instances saving the same records; retrying bulkSave after a timeout with the same (now stale) document objects; long-lived documents held across awaits while another request saves them.
Related errors
- Model.insertMany() no longer accepts a callback
- insertMany failed with ${validationErrors.length} Mongoose v
- Model.bulkWrite() no longer accepts a callback
- bulkWrite failed with ${validationErrors.length} Mongoose va
- ${operation} failed with ${validationErrors.length} Mongoose
AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21).
Data as JSON: /api/errors/6e6fda3e3f0424f3.
Report an issue: GitHub.