hibernate/hibernate-orm · error · PessimisticEntityLockException
Lock timeout exceeded attempting to lock row(s) for %s
Error message
Lock timeout exceeded attempting to lock row(s) for %s
What it means
SqlAstBasedLockingStrategy locks an entity by issuing a locking SELECT built from the SQL AST (used when the lock is applied via a separate/follow-up select, e.g. session.lock(entity, LockMode.PESSIMISTIC_WRITE)). When the database reports that the lock wait was exceeded (org.hibernate.exception.LockTimeoutException from SQLState 55P03 on PostgreSQL, 1205 on MySQL, 1222 on SQL Server, ORA-30006 on Oracle), the strategy rethrows it as PessimisticEntityLockException with this message naming the locked object. It means the row(s) were still held by another transaction when the configured wait elapsed - a genuine runtime contention signal, not a configuration defect.
Source
Thrown at hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/SqlAstBasedLockingStrategy.java:193
1,
SingleResultConsumer.instance()
);
if ( lockOptions.getLockScope() == PessimisticLockScope.EXTENDED ) {
SqmMutationStrategyHelper.visitCollectionTables( entityToLock, (attribute) -> {
final var collectionToLock = (PersistentCollection<?>) attribute.getValue( object );
LockingHelper.lockCollectionTable(
attribute,
lockMode,
lockOptions.getTimeout(),
collectionToLock,
lockingExecutionContext
);
} );
}
}
catch (LockTimeoutException lockTimeout) {
throw new PessimisticEntityLockException(
object,
String.format( Locale.ROOT, "Lock timeout exceeded attempting to lock row(s) for %s", object ),
lockTimeout
);
}
catch (NoRowException noRow) {
if ( !entityToLock.optimisticLockStyle().isNone() ) {
final String entityName = entityToLock.getEntityName();
final var statistics = session.getFactory().getStatistics();
if ( statistics.isStatisticsEnabled() ) {
statistics.optimisticFailure( entityName );
}
throw new StaleObjectStateException( entityName, id,
"No rows were returned from JDBC query for versioned entity" );
}
else {
throw noRow;
}View on GitHub (pinned to fad1729dce)
Solutions
- Catch PessimisticLockException / PessimisticEntityLockException and retry the unit of work with backoff, or surface 'busy' to the caller
- Shorten the transaction holding the lock: commit before I/O, split large transactions, lock as late as possible
- Increase the lock timeout (hint 'jakarta.persistence.lock.timeout' in ms) if the contention is transient and acceptable
- If failing fast is preferred, use a very short timeout deliberately and standardize the retry path
Example fix
// before
em.find(Order.class, id, LockModeType.PESSIMISTIC_WRITE);
// after
Order order = null;
for (int attempt = 0; attempt < 3 && order == null; attempt++) {
try {
order = em.find(Order.class, id, LockModeType.PESSIMISTIC_WRITE);
} catch (PessimisticLockException e) {
sleepBackoff(attempt); // e.g. 50ms * 2^attempt
}
} Defensive patterns
Strategy: retry
Try / catch
for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
return em.find(Order.class, id, LockModeType.PESSIMISTIC_WRITE);
} catch (PessimisticLockException e) { // wraps PessimisticEntityLockException
if (attempt == MAX_RETRIES - 1) throw e;
sleep(Duration.ofMillis(50L << attempt)); // exponential backoff
}
} Prevention
- Keep transactions short: commit before external calls, lock rows as late as possible
- Lock rows in a consistent order across transactions to reduce contention
- Index the columns used to locate locked rows to keep lock footprints small
- Size 'jakarta.persistence.lock.timeout' above your p99 transaction duration where contention is expected
When it happens
Trigger: session.lock(entity, LockMode.PESSIMISTIC_WRITE) / session.buildLockRequest(...).lock(...) on a detached or loaded entity; em.find(id, LockModeType.PESSIMISTIC_WRITE) with follow-on locking; a lock timeout (jakarta.persistence.lock.timeout in ms) smaller than the time another transaction holds the row; two concurrent transactions locking the same rows in a batch.
Common situations: Long-running transactions (user think-time inside @Transactional, external API calls while holding locks) blocking readers that lock; batch jobs contending with OLTP traffic; timeouts configured too aggressively; missing indexes causing larger lock footprints; deadlocks resolved by lock-wait expiry.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Connection lock-timeout does not accept skip-locked
- Connection lock-timeout does not accept skip-locked
- Connection lock-timeout does not accept no-wait
- Connection lock-timeout does not accept skip-locked
- Connection lock-timeout does not accept no-wait
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/1429d2eb85302d66.
Report an issue: GitHub.