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

  1. Catch OptimisticLockError and retry: reload the instance, re-apply the user's changes, and save again.
  2. Surface a 409 Conflict to the client and let them merge/re-submit.
  3. If appropriate, switch the hot path to an atomic increment (Model.increment) that does not rely on the version guard.
  4. 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

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


AI-assisted analysis of sequelize/sequelize@7e1deec499 (2026-08-03). Data as JSON: /data/errors/d091dec464f613b9.json. Report an issue: GitHub.