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

  1. Catch PessimisticLockException / PessimisticEntityLockException and retry the unit of work with backoff, or surface 'busy' to the caller
  2. Shorten the transaction holding the lock: commit before I/O, split large transactions, lock as late as possible
  3. Increase the lock timeout (hint 'jakarta.persistence.lock.timeout' in ms) if the contention is transient and acceptable
  4. 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

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

Related errors


AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22). Data as JSON: /api/errors/1429d2eb85302d66. Report an issue: GitHub.