sequelize/sequelize · error · OptimisticLockError
Attempting to update a stale model instance
Error message
Attempting to update a stale model instance: ${modelName} What it means
An OptimisticLockError thrown during instance.save() when the model has a version attribute (version: true) and the UPDATE affected zero rows. Sequelize implements optimistic locking by incrementing the version in the WHERE clause; if no row matches, another transaction modified (or deleted) the row since it was read, so this instance is stale. The error includes modelName, the attempted values, and the where clause.
Solutions
- Catch OptimisticLockError and retry: reload the instance, re-apply the user's changes, and save again.
- Surface a 409 Conflict to the client and let them merge/re-submit.
- If appropriate, switch the hot path to an atomic increment (Model.increment) that does not rely on the version guard.
- Ensure the version column is always selected (not excluded via attributes) so the lock check is accurate.
Example fix
// before
await instance.save(); // throws OptimisticLockError on conflict
// after - retry on conflict
async function saveWithRetry(instance, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await instance.save();
} catch (e) {
if (e.name !== 'SequelizeOptimisticLockError' || i === retries - 1) throw e;
await instance.reload();
}
}
} Defensive patterns
Strategy: retry
Type guard
function isOptimisticLockError(e: unknown): e is import('@sequelize/core').OptimisticLockError {
return e instanceof Error && (e as any).name === 'SequelizeOptimisticLockError';
} Try / catch
async function saveWithRetry(instance, applyChanges, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await instance.save();
} catch (e) {
if (e.name !== 'SequelizeOptimisticLockError' || i === retries - 1) throw e;
await instance.reload();
if (applyChanges) applyChanges(instance); // re-apply user intent after fresh load
}
}
} Prevention
- Always SELECT the version column when reading for update (do not exclude it via attributes).
- Catch OptimisticLockError and reload+retry or return 409 Conflict.
- For hot counters, prefer Model.increment (atomic) over read-modify-write save().
- Keep the optimistic-lock retry window small to avoid thundering herds.
When it happens
Trigger: Two concurrent transactions read the same row, both increment version, the second's UPDATE matches nothing and throws. Reading a row, holding it in memory, and saving after another request already updated it. Long-lived forms where the row changed underneath.
Common situations: High-concurrency edits on the same record (collaborative editing, counters). Tabbed editing of the same entity. Enabling version:true to protect against lost updates and surfacing conflicts. Test suites that re-save stale fixtures.
Related errors
- Sequelize is trying to add the timestamp attribute
- Sequelize is trying to add the version attribute
- findOrCreate does not support specifying which connection…
- Instance could not be reloaded because it does not exist…
- The second argument was removed in favor of the options…
AI-assisted analysis of sequelize/sequelize@7e1deec499 (2026-08-03).
Data as JSON: /api/errors/d091dec464f613b9.
Report an issue: GitHub.
Appendix: source
Thrown at packages/core/src/model.js:4098
query = 'update';
args = [this, this.constructor.table, values, where, options];
}
if (!this.changed() && !this.isNewRecord) {
return this;
}
if (this.isNewRecord) {
query = 'insert';
args = [this, this.constructor.table, values, options];
}
const [result, rowsUpdated] = await this.constructor.queryInterface[query](...args);
if (versionAttr) {
// Check to see that a row was updated, otherwise it's an optimistic locking error.
if (rowsUpdated < 1) {
throw new SequelizeErrors.OptimisticLockError({
modelName: this.constructor.name,
values,
where,
});
} else {
result.dataValues[versionAttr] = values[versionColumnName];
}
}
// Transfer database generated values (defaults, autoincrement, etc)
for (const attribute of modelDefinition.attributes.values()) {
if (
attribute.columnName &&
values[attribute.columnName] !== undefined &&
attribute.columnName !== attribute.attributeName
) {
values[attribute.attributeName] = values[attribute.columnName];
// TODO: if a column uses the same name as an attribute, this will break!View on GitHub (pinned to 7e1deec499)