hibernate/hibernate-orm · error · PessimisticEntityLockException
Could not obtain pessimistic lock
Error message
Could not obtain pessimistic lock
What it means
AbstractPessimisticUpdateLockingStrategy.lock() executes the locking UPDATE through JDBC; any SQLException surfaced as a JDBCException is wrapped in PessimisticEntityLockException with message 'Could not obtain pessimistic lock', carrying the original exception as cause. It signals the database refused or failed the lock acquisition - lock wait timeout, deadlock victim, permission or connection failure - rather than a mapping problem. The original SQLState/error code stays available via getCause().
Source
Thrown at hibernate-core/src/main/java/org/hibernate/dialect/lock/AbstractPessimisticUpdateLockingStrategy.java:58
this.lockMode = lockMode;
if ( lockMode.lessThan( LockMode.PESSIMISTIC_READ ) ) {
throw new HibernateException( "Lock mode " + lockMode
+ " not valid for locking via 'update' statement" );
}
if ( !lockable.isVersioned() ) {
throw new HibernateException( "Entity '" + lockable.getEntityName()
+ "' has no version and may not be locked via 'update' statement" );
}
this.sql = generateLockString();
}
@Override
public void lock(Object id, Object version, Object object, int timeout, SharedSessionContractImplementor session) {
try {
doLock( id, version, session );
}
catch (JDBCException e) {
throw new PessimisticEntityLockException( object, "Could not obtain pessimistic lock", e );
}
}
void doLock(Object id, Object version, SharedSessionContractImplementor session) {
try {
final var factory = session.getFactory();
final var jdbcCoordinator = session.getJdbcCoordinator();
final var preparedStatement = jdbcCoordinator.getStatementPreparer().prepareStatement( sql );
try {
final var versionType = lockable.getVersionType();
final var identifierType = lockable.getIdentifierType();
versionType.nullSafeSet( preparedStatement, version, 1, session );
int offset = 2;
identifierType.nullSafeSet( preparedStatement, id, offset, session );
offset += identifierType.getColumnSpan( factory.getRuntimeMetamodels() );
View on GitHub (pinned to fad1729dce)
Solutions
- Inspect getCause() (and its SQLState/error code) to classify: retry deadlocks and timeouts with backoff, fix permissions or connection issues instead of retrying
- Shorten the transaction that holds conflicting locks, and always lock rows in a consistent order to avoid deadlocks
- Set a lock timeout hint (e.g. LockOptions.setTimeout(Timeout.seconds(n)) or jakarta.persistence.lock.timeout) so waiting fails fast instead of the driver default
- If contention is structural, switch to optimistic locking for that use case
Example fix
// before
session.lock(person, LockMode.PESSIMISTIC_WRITE);
// after
try {
session.lock(person, LockMode.PESSIMISTIC_WRITE);
}
catch (PessimisticEntityLockException e) {
if (isDeadlockOrTimeout(e.getCause())) {
// back off and retry with jitter, then reload the entity
}
else {
throw e;
}
} Defensive patterns
Strategy: retry
Try / catch
try {
session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)
.setTimeout(Timeout.seconds(5))).lock(person);
}
catch (PessimisticEntityLockException e) { // org.hibernate.dialect.lock
Throwable cause = e.getCause();
if (cause instanceof PessimisticLockException
|| (cause instanceof JDBCException jdbc && isDeadlockOrTimeout(jdbc.getSQLException()))) {
backoffAndRetry(); // exponential backoff + jitter, reload entity after retry
}
else {
throw e;
}
} Prevention
- Always set an explicit lock timeout so contention fails fast instead of hitting driver defaults
- Acquire locks in a globally consistent order (e.g. ordered by primary key) to prevent deadlocks
- Keep lock-holding transactions short: lock, act, commit - no remote calls while holding a lock
- Classify the cause (SQLState 40001 deadlock / 55P03 lock_not_available) before deciding to retry
When it happens
Trigger: session.lock(entity, LockMode.PESSIMISTIC_WRITE) or buildLockRequest(...).lock() while another transaction holds a conflicting row lock past innodb_lock_wait_timeout / lock_timeout; being chosen deadlock victim by the database; the locking UPDATE violating permissions or the connection dying mid-statement. All surface as JDBCException inside doLock and are rethrown as PessimisticEntityLockException.
Common situations: Long-running transactions that lock popular rows (order processing, scheduler jobs); lock ordering inconsistencies across services causing deadlocks; tight retry loops that immediately re-acquire locks; connection pool exhaustion causing failures during the lock statement.
Related errors
- Row was already updated or deleted by another transaction
- Spanner does not support no wait.
- Spanner does not support skip locked.
- Spanner does not support lock timeout.
- Lock mode ${lockMode} not valid for locking via 'update' sta
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/fe13d9ca853971bf.
Report an issue: GitHub.