{"record":{"id":"6e6fda3e3f0424f3","repo":"Automattic/mongoose","slug":"modelname-bulksave-was-not-able-to-update-n","errorCode":null,"errorMessage":"${modelName}.bulkSave() was not able to update ${numDocumentsNotUpdated} of the given documents due to incorrect version or optimistic concurrency, document ids: ${preview}","messagePattern":"(.+?)\\.bulkSave\\(\\) was not able to update (.+?) of the given documents due to incorrect version or optimistic concurrency, document ids: (.+?)","errorType":"exception","errorClass":"MongooseBulkSaveIncompleteError","httpStatus":null,"severity":"error","filePath":"lib/model.js","lineNumber":3650,"sourceCode":"  }\n\n  await Promise.all(documents.map(doc => buildPreSavePromise(doc, options)));\n\n  const writeOperations = this.buildBulkWriteOperations(documents, options);\n  const opts = { skipValidation: true, _skipCastBulkWrite: true, ...options };\n  const { bulkWriteResult, bulkWriteError } = await this.bulkWrite(writeOperations, opts).then(\n    (res) => ({ bulkWriteResult: res, bulkWriteError: null }),\n    (err) => ({ bulkWriteResult: null, bulkWriteError: err })\n  );\n  // If not a MongoBulkWriteError, treat this as all documents failed to save.\n  if (bulkWriteError != null && bulkWriteError.name !== 'MongoBulkWriteError') {\n    throw bulkWriteError;\n  }\n\n  const matchedCount = bulkWriteResult?.matchedCount ?? 0;\n  const insertedCount = bulkWriteResult?.insertedCount ?? 0;\n  if (writeOperations.length > 0 && matchedCount + insertedCount < writeOperations.length && !bulkWriteError) {\n    throw new MongooseBulkSaveIncompleteError(\n      this.modelName,\n      documents,\n      bulkWriteResult\n    );\n  }\n\n  const successfulDocuments = [];\n  for (let i = 0; i < documents.length; i++) {\n    const document = documents[i];\n    const documentError = bulkWriteError?.writeErrors.find(writeError => {\n      const writeErrorDocumentId = writeError.err.op._id || writeError.err.op.q._id;\n      return writeErrorDocumentId.toString() === document._doc._id.toString();\n    });\n\n    if (documentError == null) {\n      successfulDocuments.push(document);\n    }\n  }","sourceCodeStart":3632,"sourceCodeEnd":3668,"githubUrl":"https://github.com/Automattic/mongoose/blob/49cdab01366679723b487ecb754b38570f783289/lib/model.js#L3632-L3668","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait Model.bulkSave(docs); // another worker already bumped __v on some docs -> throws\n\n// after\ntry {\n  await Model.bulkSave(docs);\n} catch (err) {\n  if (err.name !== 'MongooseBulkSaveIncompleteError') throw err;\n  const ids = err.documents.map(d => d._id);\n  const fresh = await Model.find({ _id: { $in: ids } }); // re-apply changes to fresh copies\n  for (const doc of fresh) applyChanges(doc);\n  await Model.bulkSave(fresh);\n}","handlingStrategy":"retry","validationCode":"// Reduce the stale-version window before bulkSave\nfunction assertFresh(Model, docs, toleranceMs = 5000) {\n  const stale = docs.filter(d => d.$__.saveTime != null && Date.now() - d.$__.saveTime > toleranceMs);\n  // practical version: track load time yourself\n}\n// Practical guard: reload right before saving\nconst fresh = await Model.find({ _id: { $in: docs.map(d => d._id) } });","typeGuard":null,"tryCatchPattern":"async function bulkSaveWithRetry(Model, applyChanges, ids, attempts = 3) {\n  for (let i = 0; i < attempts; i++) {\n    const docs = await Model.find({ _id: { $in: ids } }); // fresh __v every attempt\n    applyChanges(docs);\n    try {\n      return await Model.bulkSave(docs);\n    } catch (err) {\n      if (err.name !== 'MongooseBulkSaveIncompleteError' || i === attempts - 1) throw err;\n      // err.documents / err.numDocumentsNotUpdated identify what missed\n    }\n  }\n}","preventionTips":["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"],"tags":["mongoose","bulksave","versioning","optimistic-concurrency","conflict","bulk"],"backgroundTag":"optimistic-concurrency-conflict","analyzedSha":"49cdab01366679723b487ecb754b38570f783289","analyzedAt":"2026-08-21T22:54:00.882Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}