skip to content

A pessimistic read in JPA blocks indefinitely while another transaction holds the row. Which standard JPA hint bounds that wait, what special values does Hibernate recognise for it, and which exception surfaces when the wait expires?

level: seniorimportance: must knowfreq 42%

answer

  1. jakarta.persistence.lock.timeout, milliseconds
  2. 0 = NOWAIT, -2 = SKIP LOCKED, -1 = wait forever
  3. positive wait only where the dialect has syntax for it
  4. LockTimeoutException = no rollback marking
  5. PessimisticLockException (deadlock victim) = rollback

basics

~20 s

The hint jakarta.persistence.lock.timeout, in milliseconds. Hibernate reads 0 as NOWAIT (fail instantly) and -2 as SKIP LOCKED (skip contended rows); -1 waits forever. Expiry raises LockTimeoutException, which — unlike PessimisticLockException — does not mark the transaction for rollback.

solid answer

~50 s

You pass `jakarta.persistence.lock.timeout` as a property map on `find`/`lock` or as a query hint; the value is milliseconds. Hibernate gives two values special meaning: `0` renders `NOWAIT`, so the statement fails at once rather than queueing, and `-2` renders `SKIP LOCKED`, so contended rows are omitted from the result instead of waited on. `-1` means wait indefinitely. Support is dialect-driven. PostgreSQL, Oracle and MySQL 8 support `NOWAIT` and `SKIP LOCKED`; a *positive* wait is only expressible on engines with syntax for it (Oracle's `for update wait 5`), so elsewhere Hibernate falls back to a session-level lock timeout or ignores it — verify the generated SQL. On expiry you get `jakarta.persistence.LockTimeoutException`. Per the specification it does **not** mark the transaction for rollback, so you may catch it and retry or take another path; `PessimisticLockException` (a genuine lock failure such as a deadlock victim) does mark it, and must not be swallowed.

code

java · 7 lines
java
Map<String, Object> hints = Map.of("jakarta.persistence.lock.timeout", 0); // NOWAIT
try {
    Order order = em.find(Order.class, id, LockModeType.PESSIMISTIC_WRITE, hints);
    order.cancel();
} catch (LockTimeoutException busy) {
    // transaction is NOT marked for rollback: retry or report "resource busy"
}

go deeper

for a junior

Know that the hint jakarta.persistence.lock.timeout exists, that it is in milliseconds, and that 0 means fail immediately.

for a middle

Add the -2 SKIP LOCKED value, name LockTimeoutException, and note that dialect support varies.

for a senior

Lead with the two exceptions and their different rollback semantics, the per-dialect rendering of positive waits, and why unbounded locks exhaust connection pools.

for a principal

Treat lock-wait budgets as an explicit part of the operation's contract — fail-fast plus a retry policy at the caller, with SKIP LOCKED reserved for work claiming.

## The hint JPA standardises one knob: the property `jakarta.persistence.lock.timeout` (`javax.persistence.lock.timeout` before Jakarta EE 9), expressed in **milliseconds**. It can be supplied three ways: as a properties map argument to `em.find(..., lockMode, props)` or `em.lock(entity, lockMode, props)`, as a query hint via `query.setHint(...)`, or globally in persistence unit properties. Per-operation beats global. ## The magic values Hibernate interprets three values specially, matching the constants historically found on `org.hibernate.LockOptions`: - **0 — NOWAIT.** The statement is rendered `for update nowait`. If the row is locked, the database errors immediately instead of queueing. Use it when waiting is worse than failing: interactive requests, anything with a tight latency budget. - **-2 — SKIP LOCKED.** Rendered `for update skip locked`. Locked rows are silently left out of the result set. This is the queue-worker primitive: each poller takes rows nobody else has claimed rather than lining up behind them. - **-1 — wait forever.** The default behaviour of a plain `for update`. Any other positive value asks the engine to wait that long. Whether that is expressible depends entirely on the dialect: Oracle has `for update wait <seconds>`; PostgreSQL and MySQL have no per-statement wait, so the value is applied through a session-level setting or dropped. This is the single most common surprise — people set 3000 ms, never see a timeout, and conclude the hint is broken. Log the SQL and confirm. ## The exceptions, and why the difference matters Two distinct exceptions can come out: - `jakarta.persistence.LockTimeoutException` — the lock could not be obtained within the allowed time (including the immediate failure under `NOWAIT`). The specification says it does **not** mark the current transaction for rollback. That is deliberate: a timeout is a schedulability problem, not a data problem, so the application may catch it, back off, retry the acquisition, or return a "resource busy" response and still commit whatever else it legitimately did. - `jakarta.persistence.PessimisticLockException` — the lock attempt failed outright, most often because the engine chose this transaction as a deadlock victim. This one *does* mark the transaction for rollback; the only correct handling is to roll back and, if the operation is idempotent, retry the whole unit of work from scratch. Hibernate produces these by converting vendor SQL states through the dialect (for example PostgreSQL's `55P03 lock_not_available` from `NOWAIT` becomes a `LockTimeoutException`, `40P01 deadlock_detected` becomes a `PessimisticLockException`). If a driver or dialect misclassifies, you will see the generic wrapper instead — another reason to test the failure path, not just the happy path. ## Design guidance An unbounded `for update` in a request-serving path is a latency bomb: threads pile up behind one slow holder until the pool is exhausted, and the symptom appears as a thread or connection leak far from the real cause. Give user-facing locks an explicit bound — `NOWAIT` plus a clear "try again" response is usually better than a wait that eventually blows the pool. Reserve unbounded waits for background jobs where queueing is genuinely fine, and reserve `SKIP LOCKED` for work-claiming, where skipping is exactly the desired semantics. Treat the timeout as part of the API contract of the operation: whoever calls it should know it can fail fast, and the retry policy belongs at that boundary.

  • You set the lock timeout hint to 3000 on PostgreSQL and the statement still blocks forever. Why?
    PostgreSQL has no per-statement `for update wait n` syntax, so a positive millisecond value cannot be rendered into the locking clause; depending on the Hibernate version it is either applied through a session-level lock timeout setting or ignored. The values that always translate are 0 (NOWAIT) and -2 (SKIP LOCKED). Check the emitted SQL, and if you need a bounded wait set the engine's own lock timeout for that connection.
  • Can you catch LockTimeoutException and keep using the same transaction?
    Yes — the specification says a lock timeout does not mark the transaction for rollback, so catching it, waiting, and retrying the acquisition or choosing another path is legitimate. That is not true of PessimisticLockException, which signals the lock attempt failed for a reason that leaves the transaction unusable (typically a deadlock rollback), so it must be rolled back and retried as a whole.

saying these in an interview costs you the question

  • Treating the hint value as seconds rather than milliseconds.
  • Assuming a positive timeout is honoured on every database — it needs dialect syntax.
  • Catching LockTimeoutException and PessimisticLockException identically, then continuing to use a transaction the engine already rolled back.
  • Believing SKIP LOCKED waits briefly and then errors — it silently omits the contended rows.
  • Leaving unbounded FOR UPDATE waits in a user-facing path and blaming the resulting pool exhaustion on the connection pool.

context