sequelize/sequelize · error · OptimisticLockError
Attempting to update a stale model instance: ${modelName}
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.
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)
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 version attribute ${NodeUtil.
- Sequelize is trying to add the timestamp attribute ${NodeUti
- Validations already in progress.
- findOrCreate does not support specifying which connection mu
- Validation Error
AI-assisted analysis of sequelize/sequelize@7e1deec499 (2026-08-03).
Data as JSON: /data/errors/d091dec464f613b9.json.
Report an issue: GitHub.