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

  1. Handle MongooseBulkSaveIncompleteError by re-fetching the affected documents (err.documents lists them) and reapplying/retrying the change, since the in-memory copies are stale
  2. Shorten the window: bulkSave soon after loading documents, avoid holding them across long awaits
  3. Give conflicting writers disjoint document sets (shard work by _id) so version guards never collide
  4. 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
  5. 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

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


AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21). Data as JSON: /api/errors/6e6fda3e3f0424f3. Report an issue: GitHub.