n8n-io/n8n · error · OptimisticLockVersionMismatchError

The optimistic lock on entity ${entity} failed, version ${ex

Error message

The optimistic lock on entity ${entity} failed, version ${expectedVersion} was expected, but is actually ${actualVersion}.

What it means

Thrown as OptimisticLockVersionMismatchError from getOne() when lockMode is 'optimistic', lockVersion is a Date, and the entity's updateDateColumn value does not equal the expected Date. The Date-based variant compares timestamps of an @UpdateDateColumn to detect concurrent modification.

Source

Thrown at packages/@n8n/typeorm/src/query-builder/SelectQueryBuilder.ts:1564

				// means we created our own query runner
				await queryRunner.release();
		}
	}

	/**
	 * Gets single entity returned by execution of generated query builder sql.
	 */
	async getOne(): Promise<Entity | null> {
		const results = await this.getRawAndEntities();
		const result = results.entities[0] as any;

		if (result && this.expressionMap.lockMode === 'optimistic' && this.expressionMap.lockVersion) {
			const metadata = this.expressionMap.mainAlias!.metadata;

			if (this.expressionMap.lockVersion instanceof Date) {
				const actualVersion = metadata.updateDateColumn!.getEntityValue(result); // what if columns arent set?
				if (actualVersion.getTime() !== this.expressionMap.lockVersion.getTime())
					throw new OptimisticLockVersionMismatchError(
						metadata.name,
						this.expressionMap.lockVersion,
						actualVersion,
					);
			} else {
				const actualVersion = metadata.versionColumn!.getEntityValue(result); // what if columns arent set?
				if (actualVersion !== this.expressionMap.lockVersion)
					throw new OptimisticLockVersionMismatchError(
						metadata.name,
						this.expressionMap.lockVersion,
						actualVersion,
					);
			}
		}

		if (result === undefined) {
			return null;
		}

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Re-read the entity to refresh the updateDate, then retry the operation (optimistic-lock retry loop).
  2. Switch to a numeric @VersionColumn if you control the schema, for deterministic version bumps.
  3. Ensure only one writer path mutates the row, or use pessimistic locking for hot rows.

Example fix

// before
const qb = repo.createQueryBuilder().setLock('optimistic', staleUpdateDate);
const row = await qb.getOne(); // throws if row was touched
// after
let row;
try { row = await repo.createQueryBuilder().setLock('optimistic', staleUpdateDate).getOne(); }
catch (e) { if (e.name === 'OptimisticLockVersionMismatchError') { row = await repo.findOneBy({ id }); /* retry */ } else throw e; }
Defensive patterns

Strategy: retry

Type guard

function isOptimisticVersionMismatch(e: unknown): boolean {
  return e instanceof Error && e.name === 'OptimisticLockVersionMismatchError';
}

Try / catch

async function readOptimistic<T>(repo: Repository<T>, id: any, attempts = 3): Promise<T> {
  for (let i = 0; i < attempts; i++) {
    const current = await repo.findOneByOrFail({ id });
    const updateDate = (current as any).updatedAt; // @UpdateDateColumn
    try {
      return await repo.createQueryBuilder()
        .setLock('optimistic', new Date(updateDate))
        .where({ id }).getOne() as Promise<T>;
    } catch (e) {
      if (!isOptimisticVersionMismatch(e) || i === attempts - 1) throw e;
    }
  }
  throw new Error('unreachable');
}

Prevention

When it happens

Trigger: Two concurrent transactions read the same row, both call .setLock('optimistic', lastUpdateDate), and the second's getOne() finds the updateDate already advanced by the first commit. Also triggered by system-clock skew between write and read, or by manually setting the column.

Common situations: High-contention rows (counters, shared resources); a retry loop that re-reads but reuses a stale Date; tests that freeze time then advance it inconsistently; clock changes on the DB host.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/7bef75bfbb55369f. Report an issue: GitHub.