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
- Re-read the entity to refresh the updateDate, then retry the operation (optimistic-lock retry loop).
- Switch to a numeric @VersionColumn if you control the schema, for deterministic version bumps.
- 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
- Re-fetch the entity to refresh the updateDate before each retry.
- Bound the retry count and surface a conflict error to the caller when exhausted.
- Prefer numeric @VersionColumn for deterministic concurrency control where you control the schema.
- Watch for clock skew between application and database hosts.
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
- The optimistic lock can be used only with getOne() method.
- Transactions aren't supported by ${this.connection.driver.op
- SQLite only supports SERIALIZABLE and READ UNCOMMITTED isola
- Transaction method requires callback in second parameter if
- Database connection provided by a query runner was already r
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/7bef75bfbb55369f.
Report an issue: GitHub.